Human-Typer (Enhanced) - Google Docs & Slides

Types text in a human-like manner with adjustable speed, typos, and duration.

当前为 2024-09-07 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Human-Typer (Enhanced) - Google Docs & Slides
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Types text in a human-like manner with adjustable speed, typos, and duration.
// @author       ∫(Ace)³dx
// @match        https://docs.google.com/*
// @icon         https://i.imgur.com/z2gxKWZ.png
// @grant        none
// @license MIT
// ==/UserScript==

if (window.location.href.includes("docs.google.com/document/d") || window.location.href.includes("docs.google.com/presentation/d")) {
    console.log("Document opened, Human-Typer available!");

    // Create the "Human-Typer" button
    const humanTyperButton = document.createElement("div");
    humanTyperButton.textContent = "Human-Typer";
    humanTyperButton.classList.add("menu-button", "goog-control", "goog-inline-block");
    humanTyperButton.style.userSelect = "none";
    humanTyperButton.style.cursor = "pointer";
    humanTyperButton.style.padding = "10px";
    humanTyperButton.style.backgroundColor = "#1a73e8";
    humanTyperButton.style.color = "white";
    humanTyperButton.style.borderRadius = "4px";
    humanTyperButton.id = "human-typer-button";

    // Create the "Stop" button
    const stopButton = document.createElement("div");
    stopButton.textContent = "Stop";
    stopButton.classList.add("menu-button", "goog-control", "goog-inline-block");
    stopButton.style.userSelect = "none";
    stopButton.style.color = "red";
    stopButton.style.cursor = "pointer";
    stopButton.style.padding = "10px";
    stopButton.style.borderRadius = "4px";
    stopButton.id = "stop-button";
    stopButton.style.display = "none";

    // Insert the buttons into the page
    const helpMenu = document.getElementById("docs-help-menu");
    if (helpMenu) {
        helpMenu.parentNode.insertBefore(humanTyperButton, helpMenu);
        humanTyperButton.parentNode.insertBefore(stopButton, humanTyperButton.nextSibling);
    } else {
        console.error("Help menu not found.");
    }

    let cancelTyping = false;
    let typingInProgress = false;
    let lowerBoundWPM = 30; // Default lower bound WPM
    let upperBoundWPM = 60; // Default upper bound WPM
    let typingDuration = 1; // Default duration in hours
    let introduceTypos = false;
    let fluctuateSpeed = false;

    // Function to create and show the overlay
    function showOverlay() {
        return new Promise((resolve) => {
            const overlay = document.createElement("div");
            overlay.style.position = "fixed";
            overlay.style.top = "50%";
            overlay.style.left = "50%";
            overlay.style.transform = "translate(-50%, -50%)";
            overlay.style.backgroundColor = "rgba(255, 255, 255, 0.9)";
            overlay.style.padding = "20px";
            overlay.style.borderRadius = "8px";
            overlay.style.boxShadow = "0px 2px 10px rgba(0, 0, 0, 0.1)";
            overlay.style.zIndex = "9999";
            overlay.style.display = "flex";
            overlay.style.flexDirection = "column";
            overlay.style.alignItems = "center";
            overlay.style.width = "320px";

            const textField = document.createElement("textarea");
            textField.rows = "5";
            textField.cols = "40";
            textField.placeholder = "Enter your text...";
            textField.style.marginBottom = "10px";
            textField.style.width = "100%";
            textField.style.padding = "8px";
            textField.style.border = "1px solid #ccc";
            textField.style.borderRadius = "4px";
            textField.style.resize = "vertical";

            const description = document.createElement("p");
            description.textContent = "Configure typing options below:";
            description.style.fontSize = "14px";
            description.style.marginBottom = "15px";

            const durationLabel = document.createElement("label");
            durationLabel.textContent = "Duration (hours): ";
            const durationSelect = document.createElement("select");
            [1, 2, 3, 4].forEach((val) => {
                const option = document.createElement("option");
                option.value = val;
                option.textContent = `${val} hr${val > 1 ? 's' : ''}`;
                durationSelect.appendChild(option);
            });
            durationSelect.value = typingDuration;

            const randomDelayLabel = document.createElement("div");
            randomDelayLabel.style.marginBottom = "5px";

            const lowerBoundLabel = document.createElement("label");
            lowerBoundLabel.textContent = "Lower Bound (WPM): ";
            const lowerBoundInput = document.createElement("input");
            lowerBoundInput.type = "number";
            lowerBoundInput.min = "0";
            lowerBoundInput.value = lowerBoundWPM;
            lowerBoundInput.style.marginRight = "10px";
            lowerBoundInput.style.padding = "6px";
            lowerBoundInput.style.border = "1px solid #ccc";
            lowerBoundInput.style.borderRadius = "4px";

            const upperBoundLabel = document.createElement("label");
            upperBoundLabel.textContent = "Upper Bound (WPM): ";
            const upperBoundInput = document.createElement("input");
            upperBoundInput.type = "number";
            upperBoundInput.min = "0";
            upperBoundInput.value = upperBoundWPM;
            upperBoundInput.style.marginRight = "10px";
            upperBoundInput.style.padding = "6px";
            upperBoundInput.style.border = "1px solid #ccc";
            upperBoundInput.style.borderRadius = "4px";

            const typoCheckbox = document.createElement("input");
            typoCheckbox.type = "checkbox";
            typoCheckbox.id = "typo-checkbox";
            const typoLabel = document.createElement("label");
            typoLabel.textContent = "Introduce typos";
            typoLabel.htmlFor = "typo-checkbox";
            typoLabel.style.marginRight = "10px";

            const fluctuationCheckbox = document.createElement("input");
            fluctuationCheckbox.type = "checkbox";
            fluctuationCheckbox.id = "fluctuation-checkbox";
            const fluctuationLabel = document.createElement("label");
            fluctuationLabel.textContent = "Fluctuate speed";
            fluctuationLabel.htmlFor = "fluctuation-checkbox";
            fluctuationLabel.style.marginRight = "10px";

            const confirmButton = document.createElement("button");
            confirmButton.textContent = "Confirm";
            confirmButton.style.padding = "8px 16px";
            confirmButton.style.backgroundColor = "#1a73e8";
            confirmButton.style.color = "white";
            confirmButton.style.border = "none";
            confirmButton.style.borderRadius = "4px";
            confirmButton.style.cursor = "pointer";
            confirmButton.style.transition = "background-color 0.3s";

            overlay.appendChild(description);
            overlay.appendChild(textField);
            overlay.appendChild(durationLabel);
            overlay.appendChild(durationSelect);
            overlay.appendChild(randomDelayLabel);
            overlay.appendChild(lowerBoundLabel);
            overlay.appendChild(lowerBoundInput);
            overlay.appendChild(upperBoundLabel);
            overlay.appendChild(upperBoundInput);
            overlay.appendChild(document.createElement("br"));
            overlay.appendChild(typoCheckbox);
            overlay.appendChild(typoLabel);
            overlay.appendChild(fluctuationCheckbox);
            overlay.appendChild(fluctuationLabel);
            overlay.appendChild(document.createElement("br"));
            overlay.appendChild(confirmButton);
            document.body.appendChild(overlay);

            const updateRandomDelayLabel = () => {
                const charCount = textField.value.length;
                const etaLowerBound = Math.ceil((charCount * 60) / (parseInt(lowerBoundInput.value) * 5));
                const etaUpperBound = Math.ceil((charCount * 60) / (parseInt(upperBoundInput.value) * 5));
                randomDelayLabel.textContent = `ETA: ${etaLowerBound} - ${etaUpperBound} minutes`;
            };

            const handleConfirmClick = () => {
                const userInput = textField.value.trim();
                lowerBoundWPM = parseInt(lowerBoundInput.value);
                upperBoundWPM = parseInt(upperBoundInput.value);
                typingDuration = parseInt(durationSelect.value);
                introduceTypos = typoCheckbox.checked;
                fluctuateSpeed = fluctuationCheckbox.checked;

                if (userInput === "" || isNaN(lowerBoundWPM) || isNaN(upperBoundWPM) || lowerBoundWPM < 0 || upperBoundWPM < lowerBoundWPM) {
                    document.body.removeChild(overlay);
                    return;
                }

                typingInProgress = true; // Typing has started
                stopButton.style.display = "inline"; // Show the stop button
                document.body.removeChild(overlay);
                resolve({ userInput });
            };

            confirmButton.addEventListener("click", handleConfirmClick);
            textField.addEventListener("input", updateRandomDelayLabel);
            lowerBoundInput.addEventListener("input", updateRandomDelayLabel);
            upperBoundInput.addEventListener("input", updateRandomDelayLabel);
        });
    }

    humanTyperButton.addEventListener("mouseenter", () => {
        humanTyperButton.classList.add("goog-control-hover");
    });

    humanTyperButton.addEventListener("mouseleave", () => {
        humanTyperButton.classList.remove("goog-control-hover");
    });

    stopButton.addEventListener("mouseenter", () => {
        stopButton.classList.add("goog-control-hover");
    });

    stopButton.addEventListener("mouseleave", () => {
        stopButton.classList.remove("goog-control-hover");
    });

    humanTyperButton.addEventListener("click", async () => {
        if (typingInProgress) {
            console.log("Typing in progress, please wait...");
            return;
        }

        cancelTyping = false;
        stopButton.style.display = "none"; // Hide the stop button

        const { userInput } = await showOverlay();

        if (userInput !== "") {
            const input = document.querySelector(".docs-texteventtarget-iframe").contentDocument.activeElement;

            async function simulateTyping(inputElement, char, delay) {
                return new Promise((resolve) => {
                    if (cancelTyping) {
                        stopButton.style.display = "none";
                        console.log("Typing cancelled");
                        resolve();
                        return;
                    }

                    setTimeout(() => {
                        let eventObj;
                        if (char === "\n") {
                            eventObj = new KeyboardEvent("keydown", {
                                bubbles: true,
                                key: "Enter",
                                code: "Enter",
                                keyCode: 13,
                                which: 13,
                                charCode: 13,
                            });
                        } else {
                            eventObj = new KeyboardEvent("keypress", {
                                bubbles: true,
                                key: char,
                                charCode: char.charCodeAt(0),
                                keyCode: char.charCodeAt(0),
                                which: char.charCodeAt(0),
                            });
                        }

                        inputElement.dispatchEvent(eventObj);
                        console.log(`Typed: ${char}, Delay: ${delay}ms`);
                        resolve();
                    }, delay);
                });
            }

            async function typeStringWithRandomDelay(inputElement, string) {
                const words = string.split(" ");
                const totalChars = string.length;
                const startTime = Date.now();
                const totalDuration = typingDuration * 3600000; // Convert hours to milliseconds

                for (let i = 0; i < words.length; i++) {
                    if (cancelTyping) {
                        stopButton.style.display = "none";
                        console.log("Typing cancelled");
                        return;
                    }

                    const word = words[i];
                    const baseWPM = (Math.random() * (upperBoundWPM - lowerBoundWPM)) + lowerBoundWPM;
                    const delayPerWord = (60 / baseWPM) * 60000; // Convert WPM to milliseconds

                    for (let j = 0; j < word.length; j++) {
                        const char = word[j];
                        const typoChar = introduceTypos && Math.random() < 0.05 ? String.fromCharCode(Math.random() * 26 + 65) : char; // Random typo
                        const randomDelay = fluctuateSpeed ? delayPerWord * (0.5 + Math.random()) : delayPerWord;

                        await simulateTyping(inputElement, typoChar, randomDelay);
                    }

                    if (i < words.length - 1) {
                        await simulateTyping(inputElement, " ", 0); // Space between words
                    }

                    // Check if typing exceeds the duration
                    if (Date.now() - startTime > totalDuration) {
                        cancelTyping = true;
                        break;
                    }
                }

                typingInProgress = false; // Typing has finished
                stopButton.style.display = "none"; // Hide the stop button
            }

            typeStringWithRandomDelay(input, userInput);
        }
    });

} else {
    console.log("Document not open, Human-Typer not available.");
}