TypeLapse

Simulates slow, natural typing to automatically generate content in Google Docs over time

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         TypeLapse
// @namespace    https://solanaceae.xyz/
// @version      1.3
// @description  Simulates slow, natural typing to automatically generate content in Google Docs over time
// @author       Solanaceae
// @match        https://docs.google.com/*
// @license      GPLv3
// @icon         https://raw.githubusercontent.com/0xSolanaceae/TypeLapse/refs/heads/main/assets/favicon/favicon.svg
// ==/UserScript==

(function() {
    'use strict';

    if (
        window.location.href.includes("docs.google.com/document/d") ||
        window.location.href.includes("docs.google.com/presentation/d")
    ) {
        console.log("TypeLapse loaded!");

        const TypeLapseButton = createButton("TypeLapse", "type-lapse-button");
        const stopButton = createButton("Stop", "stop-button", "red");
        const pauseButton = createButton("Pause", "pause-button", "orange");
        const resumeButton = createButton("Resume", "resume-button", "green");
        stopButton.style.display = "none";
        pauseButton.style.display = "none";
        resumeButton.style.display = "none";
        stopButton.style.verticalAlign = "middle";
        stopButton.style.lineHeight = "normal";
        pauseButton.style.verticalAlign = "middle";
        pauseButton.style.lineHeight = "normal";
        resumeButton.style.verticalAlign = "middle";
        resumeButton.style.lineHeight = "normal";

        const helpMenu = document.getElementById("docs-help-menu");
        if (helpMenu) {
            helpMenu.parentNode.appendChild(TypeLapseButton);
            TypeLapseButton.parentNode.appendChild(stopButton);
            TypeLapseButton.parentNode.appendChild(pauseButton);
            TypeLapseButton.parentNode.appendChild(resumeButton);
        } else {
            console.error("Help menu not found");
            return;
        }

        let cancelTyping = false;
        let typingInProgress = false;
        let typingPaused = false;
        let lowerBoundValue = 60;
        let upperBoundValue = 120;
        let breakFrequency = 100;
        let breakDuration = 5000;
        let enableBreaks = false;
        let simulateErrors = false;

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

            cancelTyping = false;
            typingPaused = false;
            stopButton.style.display = "none";
            pauseButton.style.display = "none";
            resumeButton.style.display = "none";

            const { userInput } = await showOverlay();

            if (userInput !== "") {
                const input = document.querySelector(".docs-texteventtarget-iframe")
                    .contentDocument.activeElement;
                if (input) {
                    typeStringWithRandomDelay(input, userInput);
                } else {
                    console.error("Input element not found");
                }
            }
        });

        stopButton.addEventListener("click", () => {
            cancelTyping = true;
            stopButton.style.display = "none";
            pauseButton.style.display = "none";
            resumeButton.style.display = "none";
            console.log("Typing cancelled");
        });

        pauseButton.addEventListener("click", () => {
            typingPaused = true;
            pauseButton.style.display = "none";
            resumeButton.style.display = "inline";
            console.log("Typing paused");
        });

        resumeButton.addEventListener("click", () => {
            typingPaused = false;
            resumeButton.style.display = "none";
            pauseButton.style.display = "inline";
            console.log("Typing resumed");
        });

        window.addEventListener("beforeunload", () => {
            cancelTyping = true;
            typingInProgress = false;
        });

        window.addEventListener("load", () => {
            cancelTyping = false;
            typingInProgress = false;
            typingPaused = false;
            enableBreaks = false;
            simulateErrors = false;
            const enableBreaksCheckbox = document.querySelector("#type-lapse-overlay input[type='checkbox']");
            if (enableBreaksCheckbox) {
                enableBreaksCheckbox.checked = false;
            }
        });

        function createButton(text, id, color = "black") {
            const button = document.createElement("div");
            button.textContent = text;
            button.classList.add("menu-button", "goog-control", "goog-inline-block");
            button.style.userSelect = "none";
            button.style.color = color;
            button.style.cursor = "pointer";
            button.style.transition = "color 0.3s";
            button.id = id;
            return button;
        }

        async function showOverlay() {
            const existingOverlay = document.getElementById("type-lapse-overlay");
            if (existingOverlay) {
                document.body.removeChild(existingOverlay);
                return;
            }
        
            const overlay = document.createElement("div");
            overlay.id = "type-lapse-overlay";
            overlay.style = `
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background-color: #fff;
                padding: 20px;
                border-radius: 12px;
                box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.2);
                z-index: 9999;
                display: flex;
                flex-direction: column;
                align-items: center;
                width: 400px;
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            `;
        
            let isDragging = false;
            let offsetX, offsetY;
        
            overlay.addEventListener("mousedown", (e) => {
                if (e.target === overlay) {
                    isDragging = true;
                    offsetX = e.clientX - overlay.getBoundingClientRect().left;
                    offsetY = e.clientY - overlay.getBoundingClientRect().top;
                }
            });
        
            document.addEventListener("mousemove", (e) => {
                if (isDragging) {
                    overlay.style.left = `${e.clientX - offsetX}px`;
                    overlay.style.top = `${e.clientY - offsetY}px`;
                    overlay.style.transform = "none";
                }
            });
        
            document.addEventListener("mouseup", () => {
                isDragging = false;
            });
        
            const closeButton = document.createElement("button");
            closeButton.textContent = "✖";
            closeButton.style = `
                position: absolute;
                top: 10px;
                right: 10px;
                background: none;
                border: none;
                font-size: 18px;
                cursor: pointer;
                color: #333;
            `;
            closeButton.addEventListener("click", () => {
                document.body.removeChild(overlay);
            });
        
            const heading = document.createElement("h2");
            heading.textContent = "TypeLapse";
            heading.style = `
                margin-bottom: 15px;
                font-size: 24px;
                color: #333;
            `;
        
            const textField = document.createElement("textarea");
            textField.rows = "5";
            textField.cols = "40";
            textField.placeholder = "Enter your text...";
            textField.style = `
                margin-bottom: 10px;
                width: 100%;
                padding: 10px;
                border: 1px solid #ccc;
                border-radius: 8px;
                resize: vertical;
                font-size: 14px;
            `;
        
            const description = document.createElement("p");
            description.textContent =
                "Keep this tab open; otherwise, the script will pause and resume when you return. Each character has a random typing delay between the lower (min) and upper (max) bounds in milliseconds. Lower values are faster.";
            description.style = `
                font-size: 14px;
                margin-bottom: 15px;
                text-align: center;
            `;
        
            const randomDelayLabel = document.createElement("div");
            randomDelayLabel.style.marginBottom = "5px";
        
            const lowerBoundLabel = createLabel("Lower Bound (ms): ", lowerBoundValue);
            const upperBoundLabel = createLabel("Upper Bound (ms): ", upperBoundValue);
            const simulateErrorsLabel = createCheckboxLabel("Simulate Errors: ", simulateErrors);
            const enableBreaksLabel = createCheckboxLabel("Enable Breaks: ", enableBreaks);
        
            const breakOptionsContainer = document.createElement("div");
            breakOptionsContainer.style.display = "none";
        
            const breakDescription = document.createElement("p");
            breakDescription.textContent =
                "Breaks will pause typing for a specified duration after a certain number of characters.";
            breakDescription.style = `
                font-size: 14px;
                margin-bottom: 10px;
                text-align: center;
            `;
        
            const breakFrequencyLabel = createLabel("Break Frequency (chars): ", breakFrequency);
            const breakDurationLabel = createLabel("Break Duration (ms): ", breakDuration);
        
            breakOptionsContainer.append(
                breakDescription,
                breakFrequencyLabel,
                breakDurationLabel,
            );
        
            enableBreaksLabel.querySelector("input").addEventListener("change", () => {
                breakOptionsContainer.style.display = enableBreaksLabel.querySelector("input").checked ? "block" : "none";
            });
        
            const confirmButton = document.createElement("button");
            confirmButton.textContent = "Cancel";
            confirmButton.style = `
                padding: 10px 20px;
                background-color: #4d82f4;
                color: white;
                border: none;
                border-radius: 8px;
                cursor: pointer;
                transition: background-color 0.3s;
                font-size: 16px;
                margin-top: 10px;
            `;
            confirmButton.addEventListener("mouseover", () => {
                confirmButton.style.backgroundColor = "#3b6bd6";
            });
            confirmButton.addEventListener("mouseout", () => {
                confirmButton.style.backgroundColor = "#4d82f4";
            });
        
            overlay.append(
                closeButton,
                heading,
                description,
                textField,
                randomDelayLabel,
                lowerBoundLabel,
                upperBoundLabel,
                simulateErrorsLabel,
                enableBreaksLabel,
                breakOptionsContainer,
                confirmButton,
            );
            document.body.appendChild(overlay);
        
            return new Promise((resolve) => {
                const updateRandomDelayLabel = () => {
                    const charCount = textField.value.length;
                    const etaLowerBound = Math.ceil(
                        (charCount * parseInt(lowerBoundLabel.querySelector("input").value)) /
                        60000,
                    );
                    const etaUpperBound = Math.ceil(
                        (charCount * parseInt(upperBoundLabel.querySelector("input").value)) /
                        60000,
                    );
                    randomDelayLabel.textContent = `ETA: ${etaLowerBound} - ${etaUpperBound} minutes`;
                };
        
                confirmButton.addEventListener("click", () => {
                    const userInput = textField.value.trim();
                    lowerBoundValue = parseInt(
                        lowerBoundLabel.querySelector("input").value,
                    );
                    upperBoundValue = parseInt(
                        upperBoundLabel.querySelector("input").value,
                    );
                    breakFrequency = parseInt(
                        breakFrequencyLabel.querySelector("input").value,
                    );
                    breakDuration = parseInt(
                        breakDurationLabel.querySelector("input").value,
                    );
                    enableBreaks = enableBreaksLabel.querySelector("input").checked;
                    simulateErrors = simulateErrorsLabel.querySelector("input").checked;
        
                    if (userInput === "") {
                        document.body.removeChild(overlay);
                        return;
                    }
        
                    if (isNaN(lowerBoundValue) ||
                        isNaN(upperBoundValue) ||
                        lowerBoundValue < 0 ||
                        upperBoundValue < lowerBoundValue ||
                        isNaN(breakFrequency) ||
                        breakFrequency <= 0 ||
                        isNaN(breakDuration) ||
                        breakDuration < 0) {
                        return;
                    }
        
                    typingInProgress = true;
                    stopButton.style.display = "inline";
                    pauseButton.style.display = "inline";
                    document.body.removeChild(overlay);
                    resolve({
                        userInput,
                    });
                });
        
                textField.addEventListener("input", () => {
                    confirmButton.textContent =
                        textField.value.trim() === "" ? "Cancel" : "Confirm";
                    updateRandomDelayLabel();
                });
        
                lowerBoundLabel
                    .querySelector("input")
                    .addEventListener("input", updateRandomDelayLabel);
                upperBoundLabel
                    .querySelector("input")
                    .addEventListener("input", updateRandomDelayLabel);
            });
        }
        
        function createLabel(text, value) {
            return createInputLabel(text, "number", value);
        }
        
        function createCheckboxLabel(text, checked) {
            return createInputLabel(text, "checkbox", checked);
        }
        
        function createInputLabel(text, type, value) {
            const label = document.createElement("label");
            label.textContent = text;
            label.style = `
                display: flex;
                align-items: center;
                justify-content: space-between;
                width: 100%;
                margin-bottom: 10px;
            `;
            const input = document.createElement("input");
            input.type = type;
            if (type === "number") {
                input.min = "0";
                input.value = value;
                input.style = `
                    margin-left: 10px;
                    padding: 6px;
                    border: 1px solid #ccc;
                    border-radius: 4px;
                    width: 100px;
                `;
            } else if (type === "checkbox") {
                input.checked = value;
                input.style = `
                    margin-left: 10px;
                    transform: scale(1.5);
                `;
            }
            label.appendChild(input);
            return label;
        }

        async function typeStringWithRandomDelay(inputElement, string) {
            const keyboardLayout = {
                a: ['q', 's', 'w', 'z'], b: ['v', 'n', 'g', 'h'], c: ['x', 'v', 'd', 'f'],
                d: ['s', 'f', 'e', 'r'], e: ['w', 'r', 'd', 'f'], f: ['d', 'g', 'r', 't'],
                g: ['f', 'h', 't', 'y'], h: ['g', 'j', 'y', 'u'], i: ['u', 'o', 'j', 'k'],
                j: ['h', 'k', 'u', 'i'], k: ['j', 'l', 'i', 'o'], l: ['k', 'o', 'p'],
                m: ['n', 'j', 'k'], n: ['b', 'm', 'h', 'j'], o: ['i', 'p', 'k', 'l'],
                p: ['o', 'l'], q: ['a', 'w', 's'], r: ['e', 't', 'd', 'f'],
                s: ['a', 'd', 'q', 'w'], t: ['r', 'y', 'f', 'g'], u: ['y', 'i', 'h', 'j'],
                v: ['c', 'b', 'f', 'g'], w: ['q', 'e', 'a', 's'], x: ['z', 'c', 's', 'd'],
                y: ['t', 'u', 'g', 'h'], z: ['a', 's', 'x'],
            };
        
            for (let i = 0; i < string.length; i++) {
                if (cancelTyping) {
                    stopButton.style.display = "none";
                    pauseButton.style.display = "none";
                    resumeButton.style.display = "none";
                    console.log("Typing cancelled");
                    break;
                }
        
                if (typingPaused) {
                    await new Promise(resolve => {
                        const interval = setInterval(() => {
                            if (!typingPaused) {
                                clearInterval(interval);
                                resolve();
                            }
                        }, 100);
                    });
                }
        
                const char = string[i];
                const randomDelay =
                    Math.floor(Math.random() * (upperBoundValue - lowerBoundValue + 1)) +
                    lowerBoundValue;
        
                if (simulateErrors && Math.random() < 0.1) {
                    const errorType = Math.random();
                    if (errorType < 0.33 && keyboardLayout[char]) {
                        const neighbors = keyboardLayout[char];
                        const errorChar = neighbors[Math.floor(Math.random() * neighbors.length)];
                        await simulateTyping(inputElement, errorChar, randomDelay);
                        await simulateTyping(inputElement, "\b", randomDelay);
                    } else if (errorType < 0.66) {
                        await simulateTyping(inputElement, char, randomDelay);
                        await simulateTyping(inputElement, char, randomDelay);
                        await simulateTyping(inputElement, "\b", randomDelay);
                    } else {
                        continue;
                    }
                }
        
                await simulateTyping(inputElement, char, randomDelay);
        
                if (enableBreaks && (i + 1) % breakFrequency === 0) {
                    console.log(`Taking a break for ${breakDuration}ms`);
                    await new Promise(resolve => setTimeout(resolve, breakDuration));
                }
            }
        
            typingInProgress = false;
            stopButton.style.display = "none";
            pauseButton.style.display = "none";
            resumeButton.style.display = "none";
        }

        function simulateTyping(inputElement, char, delay) {
            return new Promise((resolve) => {
                if (cancelTyping) {
                    stopButton.style.display = "none";
                    pauseButton.style.display = "none";
                    resumeButton.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 if (char === "\b") {
                        eventObj = new KeyboardEvent("keydown", {
                            bubbles: true,
                            key: "Backspace",
                            code: "Backspace",
                            keyCode: 8,
                            which: 8,
                            charCode: 8,
                        });
                    } 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);
            });
        }
    } else {
        console.log("TypeLapse cannot find document");
    }
})();