// ==UserScript==
// @name Human-Typer (Advanced) - Google Docs & Slides
// @namespace http://tampermonkey.net/
// @version 1.4
// @description Types your text in a human-like manner with typos and variable speed. https://greasyfork.org/en/users/449798-ace-dx
// @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.setAttribute("aria-haspopup", "true");
humanTyperButton.setAttribute("aria-expanded", "false");
humanTyperButton.setAttribute("aria-disabled", "false");
humanTyperButton.setAttribute("role", "menuitem");
humanTyperButton.id = "human-typer-button";
humanTyperButton.style.transition = "color 0.3s";
// 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.transition = "color 0.3s";
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. Ensure you're on a Google Docs or Slides page.");
}
let cancelTyping = false;
let typingInProgress = false;
let lowerBoundWPM = 20; // Default lower bound WPM
let upperBoundWPM = 60; // Default upper bound WPM
let typingDuration = 60; // Default typing duration in minutes
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 = "Keep this tab open; otherwise, the script will pause and resume when you return. Customize typing speed and duration.";
description.style.fontSize = "14px";
description.style.marginBottom = "15px";
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; // Set the value from the stored variable
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; // Set the value from the stored variable
upperBoundInput.style.marginRight = "10px";
upperBoundInput.style.padding = "6px";
upperBoundInput.style.border = "1px solid #ccc";
upperBoundInput.style.borderRadius = "4px";
const durationLabel = document.createElement("label");
durationLabel.textContent = "Duration (minutes): ";
const durationSelect = document.createElement("select");
[60, 120, 180, 240].forEach((min) => {
const option = document.createElement("option");
option.value = min;
option.textContent = `${min / 60} hour${min > 60 ? 's' : ''}`;
durationSelect.appendChild(option);
});
durationSelect.value = typingDuration; // Set the value from the stored variable
const typoCheckbox = document.createElement("input");
typoCheckbox.type = "checkbox";
typoCheckbox.id = "introduce-typos";
typoCheckbox.checked = introduceTypos;
const typoLabel = document.createElement("label");
typoLabel.htmlFor = "introduce-typos";
typoLabel.textContent = "Introduce typos";
const fluctuationCheckbox = document.createElement("input");
fluctuationCheckbox.type = "checkbox";
fluctuationCheckbox.id = "fluctuate-speed";
fluctuationCheckbox.checked = fluctuateSpeed;
const fluctuationLabel = document.createElement("label");
fluctuationLabel.htmlFor = "fluctuate-speed";
fluctuationLabel.textContent = "Fluctuate typing speed";
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(randomDelayLabel);
overlay.appendChild(lowerBoundLabel);
overlay.appendChild(lowerBoundInput);
overlay.appendChild(upperBoundLabel);
overlay.appendChild(upperBoundInput);
overlay.appendChild(document.createElement("br"));
overlay.appendChild(durationLabel);
overlay.appendChild(durationSelect);
overlay.appendChild(document.createElement("br"));
overlay.appendChild(typoCheckbox);
overlay.appendChild(typoLabel);
overlay.appendChild(document.createElement("br"));
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 eta = Math.ceil((charCount / (lowerBoundWPM * 5 / 60)) / 60); // Estimate in hours
randomDelayLabel.textContent = `ETA: ${eta} hour(s)`;
};
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);
durationSelect.addEventListener("change", updateRandomDelayLabel);
});
}
function introduceTypingErrors(text) {
// Introduce random typos
let typoText = "";
for (let i = 0; i < text.length; i++) {
if (Math.random() < 0.05) { // 5% chance of typo
typoText += String.fromCharCode(Math.floor(Math.random() * 26) + 97); // Random lowercase letter
} else {
typoText += text[i];
}
}
return typoText;
}
function correctTypos(text, originalText) {
// Correct typos
let correctedText = "";
let typoIndex = 0;
for (let i = 0; i < originalText.length; i++) {
if (typoIndex < text.length && text[typoIndex] !== originalText[i]) {
correctedText += originalText[i];
typoIndex++;
} else {
correctedText += text[typoIndex] || "";
typoIndex++;
}
}
return correctedText;
}
function calculateTypingDelay(wpm) {
const charsPerWord = 5; // Average word length
const minutesPerHour = 60;
const msPerMinute = 60000;
const totalCharsPerMinute = wpm * charsPerWord;
return msPerMinute / totalCharsPerMinute;
}
function getRandomDelay() {
const baseDelay = calculateTypingDelay((Math.random() * (upperBoundWPM - lowerBoundWPM)) + lowerBoundWPM);
return fluctuateSpeed ? baseDelay * (0.5 + Math.random()) : baseDelay;
}
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 startTime = Date.now();
const endTime = startTime + (typingDuration * 60 * 60 * 1000); // Convert duration to milliseconds
let currentText = "";
while (Date.now() < endTime) {
for (let i = 0; i < string.length; i++) {
const char = string[i];
const delay = getRandomDelay();
if (cancelTyping) {
stopButton.style.display = "none";
console.log("Typing cancelled");
return;
}
await simulateTyping(inputElement, char, delay);
if (introduceTypos && Math.random() < 0.05) {
currentText = introduceTypingErrors(currentText);
}
if (correctTypos(currentText, string) === string) {
continue;
}
}
}
typingInProgress = false; // Typing has finished
stopButton.style.display = "none"; // Hide the stop button
}
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;
typeStringWithRandomDelay(input, userInput);
}
});
} else {
console.log("Document not open, Human-Typer not available.");
}