Human-Typer (Enhanced) - Google Docs & Slides

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

目前為 2024-09-07 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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.");
}