// ==UserScript==
// @name Human-Typer (Enhanced v1.7 - Advanced Algo) - Google Docs & Slides
// @namespace http://tampermonkey.net/
// @version 1.7
// @description Types text human-like with draggable UI, info popup, realistic typos, and advanced rhythm algorithm.
// @author ∫(Ace)³dx (Enhanced by Claude)
// @match https://docs.google.com/document/d/*
// @match https://docs.google.com/presentation/d/*
// @icon https://i.imgur.com/z2gxKWZ.png
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
/* globals GM_addStyle */
(function() {
'use strict';
// --- Configuration ---
const DEFAULT_LOWER_BOUND = 60;
const DEFAULT_UPPER_BOUND = 140;
const DEFAULT_TYPO_RATE_PERCENT = 5;
const DEFAULT_ENABLE_TYPOS = true;
const DEFAULT_USE_ADVANCED_ALGORITHM = true; // Enable advanced by default
// --- Basic Typo Config ---
const MAX_TYPO_LENGTH = 3;
const BASIC_TYPO_CHAR_DELAY_MS = 50;
const BASIC_TYPO_PRE_BACKSPACE_DELAY_MS = 150;
const BASIC_BACKSPACE_DELAY_MS = 90;
// --- Advanced Algorithm Config ---
const ADV_SPACE_MULTIPLIER_MIN = 1.8; // Multiplier for delay after a space
const ADV_SPACE_MULTIPLIER_MAX = 2.8;
const ADV_WORD_END_MULTIPLIER_MIN = 1.1; // Slight pause at end of word (before space/punctuation)
const ADV_WORD_END_MULTIPLIER_MAX = 1.5;
const ADV_PUNCTUATION_MULTIPLIER = 1.3; // Extra delay after typing punctuation like . , ! ?
const ADV_RANDOM_PAUSE_CHANCE = 0.02; // Chance (2%) of a brief extra pause mid-typing
const ADV_RANDOM_PAUSE_MIN_MS = 150;
const ADV_RANDOM_PAUSE_MAX_MS = 400;
// Advanced Typo Correction Delays
const ADV_TYPO_RECOGNITION_MIN_MS = 250; // Time between finishing wrong chars and starting backspace
const ADV_TYPO_RECOGNITION_MAX_MS = 800;
const ADV_BACKSPACE_DELAY_MS = 100; // Slightly slower backspacing in advanced mode
// --- State Variables ---
let cancelTyping = false;
let typingInProgress = false;
let lowerBoundValue = DEFAULT_LOWER_BOUND;
let upperBoundValue = DEFAULT_UPPER_BOUND;
let enableTypos = DEFAULT_ENABLE_TYPOS;
let typoRatePercentValue = DEFAULT_TYPO_RATE_PERCENT;
let useAdvancedAlgorithm = DEFAULT_USE_ADVANCED_ALGORITHM; // State for advanced algo
let overlayElement = null;
let infoPopupElement = null;
// --- CSS Styles ---
GM_addStyle(`
/* ... (Previous styles remain largely the same) ... */
.human-typer-overlay {
position: fixed; background-color: rgba(255, 255, 255, 0.95); padding: 0;
border-radius: 8px; box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.2); z-index: 10000;
display: flex; flex-direction: column; width: 360px; /* Slightly wider */
border: 1px solid #ccc; font-family: sans-serif; font-size: 14px; color: #333;
}
.human-typer-header {
background-color: #f1f1f1; padding: 8px 12px; cursor: move; border-bottom: 1px solid #ccc;
border-top-left-radius: 8px; border-top-right-radius: 8px; display: flex;
justify-content: space-between; align-items: center; user-select: none;
}
.human-typer-header-title { font-weight: bold; }
.human-typer-info-icon {
cursor: pointer; font-style: normal; font-weight: bold; color: #d93025; border: 1px solid #d93025;
border-radius: 50%; width: 18px; height: 18px; display: inline-flex; justify-content: center;
align-items: center; font-size: 12px; margin-left: 10px; background-color: white;
}
.human-typer-info-icon:hover { background-color: #fce8e6; }
.human-typer-content { padding: 15px; display: flex; flex-direction: column; gap: 12px; }
.human-typer-overlay textarea {
width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; resize: vertical;
box-sizing: border-box; min-height: 80px; font-family: inherit;
}
.human-typer-label { font-size: 13px; color: #555; }
.human-typer-input-group { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.human-typer-input-group label {
flex-basis: 115px; /* Adjusted label width */ text-align: right; flex-shrink: 0;
}
.human-typer-input-group input[type="number"] {
width: 60px; padding: 6px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;
}
.human-typer-options-group { /* Group checkboxes */
display: flex;
flex-direction: column; /* Stack checkboxes */
gap: 8px;
margin-top: 5px;
padding-left: 10px; /* Indent options */
}
.human-typer-checkbox-item { /* Style each checkbox line */
display: flex;
align-items: center;
gap: 8px;
}
.human-typer-checkbox-item label {
/* Labels next to checkbox don't need fixed width */
flex-basis: auto;
text-align: left;
}
.human-typer-checkbox-item input[type="number"] {
width: 55px; padding: 6px; box-sizing: border-box;
}
.human-typer-checkbox-item .rate-label { /* Specific label for rate */
margin-left: 10px;
white-space: nowrap; /* Prevent wrapping */
}
.human-typer-eta { font-size: 12px; color: #777; min-height: 1.2em; text-align: center; }
.human-typer-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; }
.human-typer-button {
padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;
transition: background-color 0.3s, color 0.3s; font-size: 14px;
}
.human-typer-confirm-button { background-color: #1a73e8; color: white; }
.human-typer-confirm-button:hover:not(:disabled) { background-color: #1765cc; }
.human-typer-confirm-button:disabled { opacity: 0.6; cursor: not-allowed; }
.human-typer-cancel-button { background-color: #e0e0e0; color: #333; }
.human-typer-cancel-button:hover { background-color: #d5d5d5; }
.human-typer-info-popup {
position: absolute; background-color: #fff; border: 1px solid #ccc; border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2); padding: 12px; font-size: 13px;
max-width: 340px; /* Slightly wider for more text */ z-index: 10001; color: #333;
}
.human-typer-info-popup p { margin-top: 0; margin-bottom: 0.7em; line-height: 1.4; }
.human-typer-info-popup p:last-child { margin-bottom: 0; }
.human-typer-info-popup strong { color: #111; }
.human-typer-info-popup code { background-color: #f0f0f0; padding: 1px 3px; border-radius: 3px; font-size: 12px;}
`);
// --- Main Logic ---
function initializeScript() { console.log("Human-Typer initializing..."); insertButtons(); }
function insertButtons() { /* ... (same as v1.6) ... */
const helpMenu = document.getElementById("docs-help-menu");
if (!helpMenu || document.getElementById("human-typer-button")) return;
const humanTyperButton = createButton("Human-Typer", "human-typer-button");
humanTyperButton.addEventListener("click", handleHumanTyperClick);
const stopButton = createButton("Stop", "stop-button", true);
stopButton.style.color = "red";
stopButton.addEventListener("click", handleStopClick);
helpMenu.parentNode.insertBefore(humanTyperButton, helpMenu);
humanTyperButton.parentNode.insertBefore(stopButton, humanTyperButton.nextSibling);
console.log("Human-Typer buttons inserted.");
}
function createButton(text, id, hidden = false) { /* ... (same as v1.6) ... */
const button = document.createElement("div");
button.textContent = text;
button.classList.add("menu-button", "goog-control", "goog-inline-block");
button.style.userSelect = "none"; button.style.cursor = "pointer"; button.style.transition = "background-color 0.2s, box-shadow 0.2s";
button.id = id; if (hidden) button.style.display = "none";
button.addEventListener("mouseenter", () => button.classList.add("goog-control-hover"));
button.addEventListener("mouseleave", () => button.classList.remove("goog-control-hover"));
return button;
}
function handleHumanTyperClick() { /* ... (same as v1.6, includes flash effect) ... */
if (typingInProgress) {
console.log("Typing already in progress.");
const stopButton = document.getElementById("stop-button");
if (stopButton) {
stopButton.style.opacity = '0.5'; setTimeout(() => { stopButton.style.opacity = '1'; }, 150);
setTimeout(() => { stopButton.style.opacity = '0.5'; }, 300); setTimeout(() => { stopButton.style.opacity = '1'; }, 450);
} return;
}
if (!overlayElement) showOverlay(); else overlayElement.style.display = 'flex';
}
function handleStopClick() { /* ... (same as v1.6) ... */
if (typingInProgress) {
console.log("Stop requested."); cancelTyping = true;
const stopButton = document.getElementById("stop-button");
if (stopButton) { stopButton.textContent = "Stopping..."; stopButton.style.cursor = "default"; stopButton.classList.remove("goog-control-hover"); }
}
}
function showOverlay() {
if (overlayElement) { overlayElement.style.display = 'flex'; return; }
overlayElement = document.createElement("div");
overlayElement.classList.add("human-typer-overlay");
// --- Header (same) ---
const header = document.createElement("div"); header.classList.add("human-typer-header");
const title = document.createElement("span"); title.classList.add("human-typer-header-title"); title.textContent = "Human-Typer Settings";
const infoIcon = document.createElement("i"); infoIcon.classList.add("human-typer-info-icon"); infoIcon.textContent = "i"; infoIcon.title = "Show Instructions";
infoIcon.addEventListener("click", toggleInfoPopup);
header.appendChild(title); header.appendChild(infoIcon); overlayElement.appendChild(header);
// --- Content Area ---
const content = document.createElement("div"); content.classList.add("human-typer-content");
const textField = document.createElement("textarea"); textField.placeholder = "Paste or type your text here...";
const etaLabel = document.createElement("div"); etaLabel.classList.add("human-typer-eta");
// --- Delay Settings ---
const delayGroup = document.createElement("div");
delayGroup.classList.add("human-typer-input-group");
const lowerBoundContainer = document.createElement("div");
const lowerBoundLabel = document.createElement("label");
lowerBoundLabel.textContent = "Min Delay (ms):";
const lowerBoundInput = document.createElement("input");
lowerBoundInput.type = "number";
lowerBoundInput.min = "0";
lowerBoundInput.value = lowerBoundValue;
lowerBoundContainer.appendChild(lowerBoundLabel);
lowerBoundContainer.appendChild(lowerBoundInput);
const upperBoundContainer = document.createElement("div");
const upperBoundLabel = document.createElement("label");
upperBoundLabel.textContent = "Max Delay (ms):";
const upperBoundInput = document.createElement("input");
upperBoundInput.type = "number";
upperBoundInput.min = "0";
upperBoundInput.value = upperBoundValue;
upperBoundContainer.appendChild(upperBoundLabel);
upperBoundContainer.appendChild(upperBoundInput);
delayGroup.appendChild(lowerBoundContainer);
delayGroup.appendChild(upperBoundContainer);
// --- Options Group (Typos + Advanced Algo) ---
const optionsGroup = document.createElement("div"); optionsGroup.classList.add("human-typer-options-group");
// --- Typo Settings ---
const typoItem = document.createElement("div"); typoItem.classList.add("human-typer-checkbox-item");
const typoCheckbox = document.createElement("input"); typoCheckbox.type = "checkbox"; typoCheckbox.id = "human-typer-typo-enable"; typoCheckbox.checked = enableTypos;
const typoLabel = document.createElement("label"); typoLabel.textContent = "Enable Typos & Auto Correction"; typoLabel.htmlFor = "human-typer-typo-enable";
const typoRateLabel = document.createElement("label"); typoRateLabel.textContent = "Typo Rate (%):"; typoRateLabel.htmlFor = "human-typer-typo-rate"; typoRateLabel.classList.add("rate-label");
const typoRateInput = document.createElement("input"); typoRateInput.type = "number"; typoRateInput.id = "human-typer-typo-rate"; typoRateInput.min = "0"; typoRateInput.max = "100"; typoRateInput.step = "1"; typoRateInput.value = typoRatePercentValue; typoRateInput.disabled = !enableTypos;
typoCheckbox.addEventListener('change', () => { typoRateInput.disabled = !typoCheckbox.checked; enableTypos = typoCheckbox.checked; updateEta(); });
typoItem.appendChild(typoCheckbox); typoItem.appendChild(typoLabel); typoItem.appendChild(typoRateLabel); typoItem.appendChild(typoRateInput);
optionsGroup.appendChild(typoItem);
// --- Advanced Algorithm Setting ---
const advancedItem = document.createElement("div"); advancedItem.classList.add("human-typer-checkbox-item");
const advancedCheckbox = document.createElement("input"); advancedCheckbox.type = "checkbox"; advancedCheckbox.id = "human-typer-advanced-algo"; advancedCheckbox.checked = useAdvancedAlgorithm;
const advancedLabel = document.createElement("label"); advancedLabel.textContent = "Use Advanced Algorithm (Rhythm/Pauses)"; advancedLabel.htmlFor = "human-typer-advanced-algo";
advancedCheckbox.addEventListener('change', () => { useAdvancedAlgorithm = advancedCheckbox.checked; updateEta(); });
advancedItem.appendChild(advancedCheckbox); advancedItem.appendChild(advancedLabel);
optionsGroup.appendChild(advancedItem);
// --- Buttons ---
const buttonContainer = document.createElement("div"); buttonContainer.classList.add("human-typer-buttons");
const confirmButton = document.createElement("button"); confirmButton.textContent = "Start Typing"; confirmButton.classList.add("human-typer-button", "human-typer-confirm-button");
const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; cancelButton.classList.add("human-typer-button", "human-typer-cancel-button");
// --- Assemble Content ---
content.appendChild(textField);
content.appendChild(etaLabel);
content.appendChild(delayGroup);
content.appendChild(optionsGroup); // Add the group of options
content.appendChild(buttonContainer);
buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(confirmButton);
overlayElement.appendChild(content);
document.body.appendChild(overlayElement);
// --- Center Overlay ---
overlayElement.style.left = `${Math.max(0, (window.innerWidth - overlayElement.offsetWidth) / 2)}px`;
overlayElement.style.top = `${Math.max(0, (window.innerHeight - overlayElement.offsetHeight) / 2)}px`;
// --- Event Listeners & ETA ---
const updateEta = () => {
const charCount = textField.value.length;
const low = parseInt(lowerBoundInput.value) || 0;
const high = parseInt(upperBoundInput.value) || 0;
if (charCount > 0 && low >= 0 && high >= low) {
let baseMs = charCount * ((low + high) / 2); // Average base time
let factor = 1.0;
if (enableTypos) {
// Estimate typo overhead (typing wrong + backspacing)
// Average typo length ~MAX_TYPO_LENGTH/2. Delay per wrong char + backspace.
const avgTypoLen = MAX_TYPO_LENGTH / 2;
const typoTimePerOccur = avgTypoLen * (BASIC_TYPO_CHAR_DELAY_MS + (useAdvancedAlgorithm ? ADV_BACKSPACE_DELAY_MS : BASIC_BACKSPACE_DELAY_MS))
+ (useAdvancedAlgorithm ? (ADV_TYPO_RECOGNITION_MIN_MS + ADV_TYPO_RECOGNITION_MAX_MS)/2 : BASIC_TYPO_PRE_BACKSPACE_DELAY_MS);
baseMs += charCount * (typoRatePercentValue / 100) * typoTimePerOccur;
}
if (useAdvancedAlgorithm) {
// Estimate overhead from pauses (spaces, ends, random) - very approximate
const spaceCount = (textField.value.match(/ /g) || []).length;
const avgSpacePauseIncrease = ((ADV_SPACE_MULTIPLIER_MIN + ADV_SPACE_MULTIPLIER_MAX) / 2 - 1) * ((low + high) / 2);
baseMs += spaceCount * avgSpacePauseIncrease;
// Add small fudge factor for other pauses
factor += 0.1;
}
const etaMinutes = Math.ceil(baseMs / 60000);
etaLabel.textContent = `ETA: ~${etaMinutes} minutes ${useAdvancedAlgorithm ? '(Advanced)' : ''} ${enableTypos ? '(incl. typos)' : ''}`;
} else {
etaLabel.textContent = "";
}
// Validate inputs
const currentTypoRate = parseInt(typoRateInput.value);
const typoRateValid = !enableTypos || (!isNaN(currentTypoRate) && currentTypoRate >= 0 && currentTypoRate <= 100); // Valid if typos disabled or rate is ok
confirmButton.disabled = textField.value.trim() === "" || low < 0 || high < low || !typoRateValid;
};
textField.addEventListener("input", updateEta);
lowerBoundInput.addEventListener("input", updateEta);
upperBoundInput.addEventListener("input", updateEta);
typoCheckbox.addEventListener("change", updateEta); // Handled above
advancedCheckbox.addEventListener("change", updateEta); // Handled above
typoRateInput.addEventListener("input", () => {
const rate = parseInt(typoRateInput.value); if (!isNaN(rate)) typoRatePercentValue = rate;
updateEta(); // Recalculate
});
cancelButton.addEventListener("click", () => { overlayElement.style.display = 'none'; hideInfoPopup(); });
confirmButton.addEventListener("click", () => {
const userInput = textField.value;
const newLower = parseInt(lowerBoundInput.value); const newUpper = parseInt(upperBoundInput.value);
const newTypoRatePercent = parseInt(typoRateInput.value);
const newEnableTypos = typoCheckbox.checked;
const newUseAdvanced = advancedCheckbox.checked;
if (userInput.trim() === "" || isNaN(newLower) || isNaN(newUpper) || newLower < 0 || newUpper < newLower || (newEnableTypos && (isNaN(newTypoRatePercent) || newTypoRatePercent < 0 || newTypoRatePercent > 100))) {
console.warn("Invalid input or settings."); return;
}
lowerBoundValue = newLower; upperBoundValue = newUpper;
enableTypos = newEnableTypos; typoRatePercentValue = newTypoRatePercent;
useAdvancedAlgorithm = newUseAdvanced; // Store advanced setting
overlayElement.style.display = 'none'; hideInfoPopup();
startTypingProcess(userInput);
});
makeDraggable(overlayElement, header); // Make draggable
updateEta(); // Initial calculation
}
function toggleInfoPopup(event) { /* ... (same as v1.6) ... */ if (infoPopupElement) hideInfoPopup(); else showInfoPopup(event.target); event.stopPropagation(); }
function showInfoPopup(iconElement) {
hideInfoPopup();
infoPopupElement = document.createElement('div');
infoPopupElement.classList.add('human-typer-info-popup');
infoPopupElement.innerHTML = `
<p><strong>Instructions:</strong></p>
<p>- Paste text into the area.</p>
<p>- Set <strong>Min/Max Delay</strong> (ms) for base character typing speed.</p>
<p>- Enable <strong>Typos</strong> & set <strong>% Rate</strong>. Typos involve typing adjacent keys, pausing, then auto-correcting with Backspace.</p>
<p>- Enable <strong>Advanced Algorithm</strong> for more human-like rhythm:</p>
<p style="margin-left: 15px; margin-bottom: 0.3em;">• Longer pauses after spaces.</p>
<p style="margin-left: 15px; margin-bottom: 0.3em;">• Slight pauses before punctuation/end-of-word.</p>
<p style="margin-left: 15px; margin-bottom: 0.3em;">• Longer, variable pause before correcting typos (<code>${ADV_TYPO_RECOGNITION_MIN_MS}-${ADV_TYPO_RECOGNITION_MAX_MS}ms</code>).</p>
<p style="margin-left: 15px; margin-bottom: 0.7em;">• Occasional random brief pauses.</p>
<p>- Click <strong>'Start Typing'</strong> (ensure cursor is in Doc/Slide).</p>
<p>- Keep tab active. Use <strong>'Stop'</strong> button to cancel.</p>
<p>- Drag window header to move.</p>
`;
document.body.appendChild(infoPopupElement);
// Position the popup (same logic as v1.6)
const iconRect = iconElement.getBoundingClientRect(); const popupRect = infoPopupElement.getBoundingClientRect();
let top = iconRect.bottom + window.scrollY + 5; let left = iconRect.left + window.scrollX - (popupRect.width / 2) + (iconRect.width / 2);
const margin = 10; if (left < margin) left = margin; if (left + popupRect.width > window.innerWidth - margin) left = window.innerWidth - popupRect.width - margin;
if (top + popupRect.height > window.innerHeight - margin) top = iconRect.top + window.scrollY - popupRect.height - 5; if (top < margin) top = margin;
infoPopupElement.style.top = `${top}px`; infoPopupElement.style.left = `${left}px`;
setTimeout(() => { document.addEventListener('click', handleClickOutsideInfoPopup, true); }, 0);
}
function hideInfoPopup() { /* ... (same as v1.6) ... */ if (infoPopupElement) { infoPopupElement.remove(); infoPopupElement = null; document.removeEventListener('click', handleClickOutsideInfoPopup, true); } }
function handleClickOutsideInfoPopup(event) { /* ... (same as v1.6) ... */ if (infoPopupElement && !infoPopupElement.contains(event.target) && !event.target.classList.contains('human-typer-info-icon')) hideInfoPopup(); }
function makeDraggable(element, handle) { /* ... (same as v1.6, includes boundary checks) ... */
let isDragging = false; let offsetX, offsetY;
const onMouseDown = (e) => { if (e.button !== 0) return; if (!handle || handle.contains(e.target)) { isDragging = true; const rect = element.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; element.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; e.preventDefault(); } };
const onMouseMove = (e) => { if (!isDragging) return; let newX = e.clientX - offsetX; let newY = e.clientY - offsetY; const vw = window.innerWidth; const vh = window.innerHeight; const elemWidth = element.offsetWidth; const elemHeight = element.offsetHeight; const margin = 5; newX = Math.max(margin, Math.min(newX, vw - elemWidth - margin)); newY = Math.max(margin, Math.min(newY, vh - elemHeight - margin)); element.style.left = `${newX}px`; element.style.top = `${newY}px`; };
const onMouseUp = (e) => { if (isDragging && e.button === 0) { isDragging = false; element.style.cursor = ''; document.body.style.userSelect = ''; if (handle) handle.style.cursor = 'move'; } };
const target = handle || element; target.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); if (handle) handle.style.cursor = 'move';
}
// --- Typing Simulation ---
async function startTypingProcess(textToType) { /* ... (same setup as v1.6) ... */
const inputElement = findInputElement(); if (!inputElement) { alert("Could not find the Google Docs/Slides input area. Ensure cursor is active."); return; }
typingInProgress = true; cancelTyping = false; const stopButton = document.getElementById("stop-button");
if (stopButton) { stopButton.textContent = "Stop"; stopButton.style.display = "inline-block"; stopButton.style.cursor = "pointer"; }
await typeStringWithLogic(inputElement, textToType);
typingInProgress = false; if (stopButton) stopButton.style.display = "none";
console.log(cancelTyping ? "Typing stopped by user." : "Typing finished.");
}
function findInputElement() { /* ... (same as v1.6) ... */
try { const iframe = document.querySelector(".docs-texteventtarget-iframe"); if (iframe && iframe.contentDocument) { return iframe.contentDocument.activeElement && iframe.contentDocument.activeElement.nodeName !== 'HTML' ? iframe.contentDocument.activeElement : iframe.contentDocument.body; } } catch (e) { console.error("Error accessing iframe content:", e); } console.error("Could not find target input element."); return null;
}
// Helper function for delays, cancellable
async function delay(ms) { /* ... (same as v1.6) ... */
return new Promise(resolve => { if (ms <= 0) { resolve(!cancelTyping); return; } const timeoutId = setTimeout(() => resolve(!cancelTyping), ms); const checkCancel = () => { if (cancelTyping) { clearTimeout(timeoutId); resolve(false); } else if (typingInProgress) { requestAnimationFrame(checkCancel); } }; requestAnimationFrame(checkCancel); });
}
// Calculates delay based on mode and context
function calculateDelay(currentChar, prevChar, nextChar) {
let baseDelay = Math.random() * (upperBoundValue - lowerBoundValue) + lowerBoundValue;
let finalDelay = baseDelay;
if (useAdvancedAlgorithm) {
const isSpace = (char) => char === ' ';
const isPunctuation = (char) => /[.,!?;:]/.test(char); // Basic punctuation
const isEndOfWord = (current, next) => !isSpace(current) && (isSpace(next) || next === null || next === '\n');
// Pause after space
if (prevChar && isSpace(prevChar)) {
finalDelay *= Math.random() * (ADV_SPACE_MULTIPLIER_MAX - ADV_SPACE_MULTIPLIER_MIN) + ADV_SPACE_MULTIPLIER_MIN;
}
// Pause after punctuation
else if (prevChar && isPunctuation(prevChar)) {
finalDelay *= ADV_PUNCTUATION_MULTIPLIER;
}
// Slight pause at end of word (before space/newline/end)
else if (isEndOfWord(currentChar, nextChar)) {
finalDelay *= Math.random() * (ADV_WORD_END_MULTIPLIER_MAX - ADV_WORD_END_MULTIPLIER_MIN) + ADV_WORD_END_MULTIPLIER_MIN;
}
// Occasional random longer pause
if (Math.random() < ADV_RANDOM_PAUSE_CHANCE) {
const pause = Math.random() * (ADV_RANDOM_PAUSE_MAX_MS - ADV_RANDOM_PAUSE_MIN_MS) + ADV_RANDOM_PAUSE_MIN_MS;
console.log(`-- Random pause: ${pause.toFixed(0)}ms --`);
finalDelay += pause;
}
}
// Ensure delay isn't negative or excessively small
return Math.max(10, finalDelay);
}
async function simulateKey(inputElement, charOrCode, keyDelay) {
// Use the delay helper BEFORE dispatching
const proceed = await delay(keyDelay);
if (!proceed) return false; // Stop if cancelled during delay
const eventProps = { bubbles: true, cancelable: true };
let eventType; let keyEventProps; let logChar = charOrCode;
if (charOrCode === '\n') {
eventType = 'keydown'; keyEventProps = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13 }; logChar = '\\n';
} else if (charOrCode === '\b') { // Backspace
eventType = 'keydown'; keyEventProps = { key: 'Backspace', code: 'Backspace', keyCode: 8, which: 8 }; logChar = 'Backspace';
}
else { // Regular character
eventType = 'keypress'; keyEventProps = { key: charOrCode, charCode: charOrCode.charCodeAt(0), keyCode: charOrCode.charCodeAt(0), which: charOrCode.charCodeAt(0) };
}
Object.assign(eventProps, keyEventProps);
const eventObj = new KeyboardEvent(eventType, eventProps);
try {
inputElement.dispatchEvent(eventObj);
console.log(`Key: ${logChar}, Delay: ${keyDelay.toFixed(0)}ms`);
} catch (e) {
console.error(`Error dispatching event for key "${logChar}":`, e);
return false; // Indicate failure
}
return true; // Indicate success
}
// Main typing loop using the logic/delay calculation
async function typeStringWithLogic(inputElement, string) {
for (let i = 0; i < string.length; i++) {
if (cancelTyping) break;
const char = string[i];
const prevChar = i > 0 ? string[i - 1] : null;
const nextChar = i < string.length - 1 ? string[i + 1] : null;
let proceed = true;
// --- Typo Simulation ---
if (enableTypos && char.match(/\S/) && char !== '\n' && Math.random() < (typoRatePercentValue / 100)) {
const typoLength = Math.floor(Math.random() * MAX_TYPO_LENGTH) + 1;
let wrongSequence = "";
for (let j = 0; j < typoLength; j++) {
wrongSequence += getNearbyKey(char); // Use original char for adjacency basis
}
console.log(`-> Simulating ${typoLength}-char typo for '${char}', typing '${wrongSequence}'`);
// 1. Type the wrong sequence
for (let j = 0; j < wrongSequence.length; j++) {
proceed = await simulateKey(inputElement, wrongSequence[j], BASIC_TYPO_CHAR_DELAY_MS); // Quick typing for wrong chars
if (!proceed) break;
}
if (!proceed) break;
// 2. Pause before correcting (variable if advanced)
const recognitionDelay = useAdvancedAlgorithm
? Math.random() * (ADV_TYPO_RECOGNITION_MAX_MS - ADV_TYPO_RECOGNITION_MIN_MS) + ADV_TYPO_RECOGNITION_MIN_MS
: BASIC_TYPO_PRE_BACKSPACE_DELAY_MS;
console.log(`-- Typo recognition pause: ${recognitionDelay.toFixed(0)}ms --`);
proceed = await delay(recognitionDelay);
if (!proceed) break;
// 3. Delete the wrong sequence
const backspaceDelay = useAdvancedAlgorithm ? ADV_BACKSPACE_DELAY_MS : BASIC_BACKSPACE_DELAY_MS;
for (let j = 0; j < wrongSequence.length; j++) {
proceed = await simulateKey(inputElement, '\b', backspaceDelay); // Use backspace delay
if (!proceed) break;
}
if (!proceed) break;
console.log(`<- Typo for '${char}' corrected.`);
}
// --- End Typo Simulation ---
// Calculate delay for the *correct* character using context
const typingDelay = calculateDelay(char, prevChar, nextChar);
// Type the correct character
proceed = await simulateKey(inputElement, char, typingDelay);
if (!proceed) break;
}
}
// --- Typo Helper ---
function getNearbyKey(char) { /* ... (same as v1.6, includes number/symbol attempt) ... */
const keyboardLayout={'q':'wa','w':'qase','e':'wsdr','r':'edft','t':'rfgy','y':'tghu','u':'yhji','i':'ujko','o':'iklp','p':'ol[','a':'qwsz','s':'awedxz','d':'erfcxs','f':'rtgvcd','g':'tyhbvf','h':'yujnbg','j':'uikmnh','k':'iolmj','l':'opk;','z':'asx','x':'zsdc','c':'xdfv','v':'cfgb','b':'vghn','n':'bhjm','m':'njk,','1':'2q`','2':'1qw3','3':'2we4','4':'3er5','5':'4rt6','6':'5ty7','7':'6yu8','8':'7ui9','9':'8io0','0':'9op-','-':'0p[=','=':'-[]','[':'=p]o',']':'[\\;p','\\':']=',';':'lkp\'[]',"'":';l/',',':'mkj.','m':'.', '/':'\'l;.,', '`':'1', ' ':' '}; // Added space mapping to itself
const lowerChar = char.toLowerCase(); const adjacent = keyboardLayout[lowerChar];
if (!adjacent || adjacent.length === 0) return char;
let attempts = 0; let nearbyChar;
do { const randomIndex = Math.floor(Math.random() * adjacent.length); nearbyChar = adjacent[randomIndex]; attempts++; } while (nearbyChar === lowerChar && attempts < 5 && adjacent.length > 1)
return char === char.toUpperCase() && char !== lowerChar ? nearbyChar.toUpperCase() : nearbyChar;
}
// --- Initialization ---
const initInterval = setInterval(() => { /* ... (same robust check as v1.6) ... */
if (document.getElementById("docs-help-menu") && document.querySelector(".docs-texteventtarget-iframe")) {
try { if (document.querySelector(".docs-texteventtarget-iframe").contentDocument) { clearInterval(initInterval); initializeScript(); } else { console.log("Human-Typer: Waiting for iframe content access..."); } } catch (e) { console.log("Human-Typer: Waiting for iframe content access (error)..."); }
} }, 500);
})(); // End userscript