Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Clean Paste - Remove and Format Text</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| animation: { | |
| 'fade-in': 'fadeIn 0.3s ease-in-out', | |
| 'fade-out': 'fadeOut 0.3s ease-in-out', | |
| 'pulse-slow': 'pulse 3s infinite', | |
| }, | |
| keyframes: { | |
| fadeIn: { | |
| '0%': { opacity: '0' }, | |
| '100%': { opacity: '1' }, | |
| }, | |
| fadeOut: { | |
| '0%': { opacity: '1' }, | |
| '100%': { opacity: '0' }, | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .text-area-container { | |
| position: relative; | |
| transition: all 0.3s ease; | |
| } | |
| .text-area-container:focus-within { | |
| box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); | |
| } | |
| .paste-btn { | |
| transition: all 0.2s ease; | |
| } | |
| .paste-btn:hover { | |
| transform: translateY(-1px); | |
| } | |
| .copy-btn { | |
| transition: all 0.2s ease; | |
| } | |
| .copy-btn:hover { | |
| transform: translateY(-1px); | |
| } | |
| .format-btn { | |
| transition: all 0.2s ease; | |
| } | |
| .format-btn:hover { | |
| transform: translateY(-1px); | |
| } | |
| .character-count { | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .tooltip { | |
| position: absolute; | |
| top: -40px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background-color: #333; | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| pointer-events: none; | |
| white-space: nowrap; | |
| } | |
| .tooltip:after { | |
| content: ""; | |
| position: absolute; | |
| top: 100%; | |
| left: 50%; | |
| margin-left: -5px; | |
| border-width: 5px; | |
| border-style: solid; | |
| border-color: #333 transparent transparent transparent; | |
| } | |
| .show-tooltip { | |
| opacity: 1; | |
| } | |
| .active-format { | |
| background-color: #3b82f6 ; | |
| color: white ; | |
| } | |
| .dark .text-area-container { | |
| background-color: #1e293b; | |
| border-color: #334155; | |
| } | |
| .dark .text-area-container textarea { | |
| background-color: #1e293b; | |
| border-color: #334155; | |
| color: #f8fafc; | |
| } | |
| .dark .text-area-container textarea::placeholder { | |
| color: #64748b; | |
| } | |
| .dark .format-btn { | |
| background-color: #334155; | |
| color: #e2e8f0; | |
| } | |
| .dark .format-btn:hover { | |
| background-color: #475569; | |
| } | |
| .dark .bg-gray-50 { | |
| background-color: #1e293b; | |
| } | |
| .dark .bg-white { | |
| background-color: #0f172a; | |
| } | |
| .dark .text-gray-600 { | |
| color: #94a3b8; | |
| } | |
| .dark .text-gray-500 { | |
| color: #94a3b8; | |
| } | |
| .dark .text-gray-700 { | |
| color: #e2e8f0; | |
| } | |
| .dark .border-gray-200 { | |
| border-color: #334155; | |
| } | |
| .code-btn { | |
| background-color: #f59e0b; | |
| color: white; | |
| } | |
| .code-btn:hover { | |
| background-color: #d97706; | |
| } | |
| .dark .code-btn { | |
| background-color: #92400e; | |
| } | |
| .dark .code-btn:hover { | |
| background-color: #7c2d12; | |
| } | |
| .analysis-panel { | |
| max-height: 0; | |
| overflow: hidden; | |
| transition: max-height 0.3s ease-out; | |
| } | |
| .analysis-panel.open { | |
| max-height: 300px; | |
| } | |
| .word-frequency-item { | |
| transition: all 0.2s ease; | |
| } | |
| .word-frequency-item:hover { | |
| transform: translateX(3px); | |
| } | |
| .find-replace-panel { | |
| max-height: 0; | |
| overflow: hidden; | |
| transition: max-height 0.3s ease-out; | |
| } | |
| .find-replace-panel.open { | |
| max-height: 200px; | |
| } | |
| .highlight { | |
| background-color: rgba(255, 255, 0, 0.4); | |
| } | |
| .current-highlight { | |
| background-color: rgba(255, 165, 0, 0.6); | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { background-color: rgba(255, 165, 0, 0.6); } | |
| 50% { background-color: rgba(255, 165, 0, 0.3); } | |
| 100% { background-color: rgba(255, 165, 0, 0.6); } | |
| } | |
| .speech-controls { | |
| max-height: 0; | |
| overflow: hidden; | |
| transition: max-height 0.3s ease-out; | |
| } | |
| .speech-controls.open { | |
| max-height: 120px; | |
| } | |
| .voice-active { | |
| animation: pulse-slow 2s infinite; | |
| } | |
| .progress-bar { | |
| height: 4px; | |
| background-color: #e5e7eb; | |
| border-radius: 2px; | |
| overflow: hidden; | |
| } | |
| .progress-bar-fill { | |
| height: 100%; | |
| background-color: #3b82f6; | |
| transition: width 0.1s linear; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 dark:bg-gray-900 min-h-screen flex flex-col items-center justify-center p-4 transition-colors duration-200"> | |
| <div class="w-full max-w-3xl bg-white dark:bg-slate-800 rounded-xl shadow-lg overflow-hidden transition-colors duration-200"> | |
| <div class="bg-blue-600 p-4"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-2"> | |
| <i class="fas fa-broom text-white text-2xl"></i> | |
| <h1 class="text-white text-2xl font-bold">Clean Paste</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <span class="text-blue-100 text-sm hidden md:block">Paste → Clean → Format → Copy</span> | |
| <button id="theme-toggle" class="text-blue-100 hover:text-white focus:outline-none"> | |
| <i class="fas fa-moon" id="theme-icon"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <p class="text-blue-100 mt-1 text-sm">Remove formatting and apply new styles to your text</p> | |
| </div> | |
| <div class="p-6"> | |
| <div class="text-area-container bg-gray-50 dark:bg-slate-700 rounded-lg border border-gray-200 dark:border-slate-600 p-4"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <label for="clean-text" class="text-gray-600 dark:text-gray-300 font-medium">Paste your formatted text here:</label> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-xs text-gray-500 dark:text-gray-400 character-count">0 characters</span> | |
| <button id="paste-btn" class="paste-btn bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-paste mr-1"></i> Paste | |
| </button> | |
| </div> | |
| </div> | |
| <textarea | |
| id="clean-text" | |
| class="w-full h-64 p-3 bg-white dark:bg-slate-700 border border-gray-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none dark:text-white" | |
| placeholder="Paste your formatted text here (or click the Paste button above)..." | |
| spellcheck="false" | |
| ></textarea> | |
| </div> | |
| <!-- Find & Replace Panel --> | |
| <div id="find-replace-panel" class="find-replace-panel bg-gray-50 dark:bg-slate-700 rounded-lg mt-4 p-4 border border-gray-200 dark:border-slate-600"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Find:</label> | |
| <div class="flex"> | |
| <input type="text" id="find-input" class="flex-1 p-2 border border-gray-300 dark:border-slate-600 rounded-l-md dark:bg-slate-600 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <button id="find-prev-btn" class="bg-gray-200 dark:bg-slate-600 px-3 border-t border-b border-gray-300 dark:border-slate-600"> | |
| <i class="fas fa-chevron-up"></i> | |
| </button> | |
| <button id="find-next-btn" class="bg-gray-200 dark:bg-slate-600 px-3 border border-gray-300 dark:border-slate-600 rounded-r-md"> | |
| <i class="fas fa-chevron-down"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Replace with:</label> | |
| <div class="flex"> | |
| <input type="text" id="replace-input" class="flex-1 p-2 border border-gray-300 dark:border-slate-600 rounded-l-md dark:bg-slate-600 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <button id="replace-btn" class="bg-blue-500 text-white px-3 border border-blue-500 rounded-r-md hover:bg-blue-600"> | |
| Replace | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center mt-3"> | |
| <div class="flex items-center space-x-2"> | |
| <input type="checkbox" id="case-sensitive" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded dark:bg-slate-600 dark:border-slate-500"> | |
| <label for="case-sensitive" class="text-sm text-gray-700 dark:text-gray-300">Case sensitive</label> | |
| </div> | |
| <div class="text-sm text-gray-500 dark:text-gray-400"> | |
| <span id="find-results">0 matches</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Text to Speech Panel --> | |
| <div id="speech-panel" class="speech-controls bg-gray-50 dark:bg-slate-700 rounded-lg mt-4 p-4 border border-gray-200 dark:border-slate-600"> | |
| <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> | |
| <div class="flex-1"> | |
| <div class="flex items-center space-x-3 mb-2"> | |
| <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Voice:</label> | |
| <select id="voice-select" class="flex-1 p-2 border border-gray-300 dark:border-slate-600 rounded-md dark:bg-slate-600 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"> | |
| <option value="">Loading voices...</option> | |
| </select> | |
| </div> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <div> | |
| <label class="text-sm font-medium text-gray-700 dark:text-gray-300 block mb-1">Rate:</label> | |
| <input type="range" id="rate-control" min="0.5" max="2" step="0.1" value="1" class="w-full"> | |
| <div class="text-xs text-gray-500 dark:text-gray-400 text-center" id="rate-value">1.0</div> | |
| </div> | |
| <div> | |
| <label class="text-sm font-medium text-gray-700 dark:text-gray-300 block mb-1">Pitch:</label> | |
| <input type="range" id="pitch-control" min="0.5" max="2" step="0.1" value="1" class="w-full"> | |
| <div class="text-xs text-gray-500 dark:text-gray-400 text-center" id="pitch-value">1.0</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex flex-col items-center justify-center space-y-2"> | |
| <div class="flex space-x-3"> | |
| <button id="speak-btn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-full flex items-center"> | |
| <i class="fas fa-play mr-2"></i> Speak | |
| </button> | |
| <button id="pause-btn" class="bg-yellow-500 hover:bg-yellow-600 text-white px-4 py-2 rounded-full flex items-center"> | |
| <i class="fas fa-pause mr-2"></i> Pause | |
| </button> | |
| <button id="stop-btn" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-full flex items-center"> | |
| <i class="fas fa-stop mr-2"></i> Stop | |
| </button> | |
| </div> | |
| <div class="progress-bar w-full max-w-xs"> | |
| <div id="progress-bar-fill" class="progress-bar-fill" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Text Analysis Panel --> | |
| <div id="analysis-panel" class="analysis-panel bg-gray-50 dark:bg-slate-700 rounded-lg mt-4 p-4 border border-gray-200 dark:border-slate-600 overflow-y-auto"> | |
| <h3 class="font-medium text-gray-700 dark:text-gray-300 mb-3">Text Analysis</h3> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4"> | |
| <div class="bg-white dark:bg-slate-600 p-3 rounded-lg shadow"> | |
| <div class="text-sm text-gray-500 dark:text-gray-400">Characters</div> | |
| <div class="text-xl font-bold text-gray-800 dark:text-white" id="analysis-chars">0</div> | |
| </div> | |
| <div class="bg-white dark:bg-slate-600 p-3 rounded-lg shadow"> | |
| <div class="text-sm text-gray-500 dark:text-gray-400">Words</div> | |
| <div class="text-xl font-bold text-gray-800 dark:text-white" id="analysis-words">0</div> | |
| </div> | |
| <div class="bg-white dark:bg-slate-600 p-3 rounded-lg shadow"> | |
| <div class="text-sm text-gray-500 dark:text-gray-400">Sentences</div> | |
| <div class="text-xl font-bold text-gray-800 dark:text-white" id="analysis-sentences">0</div> | |
| </div> | |
| <div class="bg-white dark:bg-slate-600 p-3 rounded-lg shadow"> | |
| <div class="text-sm text-gray-500 dark:text-gray-400">Reading Time</div> | |
| <div class="text-xl font-bold text-gray-800 dark:text-white" id="analysis-reading-time">0 min</div> | |
| </div> | |
| </div> | |
| <div> | |
| <h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2">Word Frequency</h4> | |
| <div class="max-h-32 overflow-y-auto" id="word-frequency-list"> | |
| <div class="text-center text-gray-500 dark:text-gray-400 py-2">No words to analyze</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <div class="flex flex-wrap gap-2 mb-4"> | |
| <button id="lowercase-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-text-height mr-1"></i> lowercase | |
| </button> | |
| <button id="uppercase-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-text-height mr-1 transform rotate-180"></i> UPPERCASE | |
| </button> | |
| <button id="bold-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-bold mr-1"></i> Bold | |
| </button> | |
| <button id="italic-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-italic mr-1"></i> Italic | |
| </button> | |
| <button id="underline-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-underline mr-1"></i> Underline | |
| </button> | |
| <button id="capitalize-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-paragraph mr-1"></i> Capitalize | |
| </button> | |
| <button id="reverse-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-exchange-alt mr-1"></i> Reverse | |
| </button> | |
| <button id="encode-btn" class="format-btn code-btn px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-lock mr-1"></i> Encode | |
| </button> | |
| <button id="decode-btn" class="format-btn code-btn px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-lock-open mr-1"></i> Decode | |
| </button> | |
| <button id="remove-format-btn" class="format-btn bg-red-100 hover:bg-red-200 dark:bg-red-900 dark:hover:bg-red-800 text-red-700 dark:text-red-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-eraser mr-1"></i> Remove Format | |
| </button> | |
| <button id="find-replace-btn" class="format-btn bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800 text-purple-700 dark:text-purple-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-search mr-1"></i> Find & Replace | |
| </button> | |
| <button id="analyze-btn" class="format-btn bg-green-100 hover:bg-green-200 dark:bg-green-900 dark:hover:bg-green-800 text-green-700 dark:text-green-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-chart-bar mr-1"></i> Analyze Text | |
| </button> | |
| <button id="speak-toggle-btn" class="format-btn bg-indigo-100 hover:bg-indigo-200 dark:bg-indigo-900 dark:hover:bg-indigo-800 text-indigo-700 dark:text-indigo-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-volume-up mr-1"></i> Text to Speech | |
| </button> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <div class="flex items-center space-x-2"> | |
| <button id="clear-btn" class="bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-md flex items-center"> | |
| <i class="fas fa-trash-alt mr-2"></i> Clear | |
| </button> | |
| <button id="copy-btn" class="copy-btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md flex items-center relative"> | |
| <i class="fas fa-copy mr-2"></i> Copy Text | |
| <span class="tooltip">Copied to clipboard!</span> | |
| </button> | |
| </div> | |
| <div class="text-xs text-gray-500 dark:text-gray-400"> | |
| <span id="word-count">0 words</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 dark:bg-slate-700 p-4 border-t border-gray-200 dark:border-slate-600"> | |
| <div class="flex flex-wrap items-center justify-center gap-4 text-sm text-gray-600 dark:text-gray-300"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-info-circle mr-1"></i> | |
| <span>Formats removed: bold, italic, colors, fonts, etc.</span> | |
| </div> | |
| <div class="flex items-center"> | |
| <i class="fas fa-keyboard mr-1"></i> | |
| <span>Shortcut: Ctrl+V to paste, Ctrl+C to copy</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-8 text-center text-gray-500 dark:text-gray-400 text-sm"> | |
| <p>Clean Paste - Remove formatting and apply new styles to your text</p> | |
| </div> | |
| <audio id="click-sound" preload="auto"> | |
| <source src="https://electronicenergysource.com/entirelibofsounds/Salvaged%20Files/1001%20Sound%20Effects/Video%20Game%20Sounds/Arcade%20Beep%2001.wav" type="audio/wav"> | |
| </audio> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const cleanTextArea = document.getElementById('clean-text'); | |
| const pasteBtn = document.getElementById('paste-btn'); | |
| const copyBtn = document.getElementById('copy-btn'); | |
| const clearBtn = document.getElementById('clear-btn'); | |
| const charCount = document.querySelector('.character-count'); | |
| const wordCount = document.getElementById('word-count'); | |
| const tooltip = document.querySelector('.tooltip'); | |
| const clickSound = document.getElementById('click-sound'); | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| const themeIcon = document.getElementById('theme-icon'); | |
| // Format buttons | |
| const lowercaseBtn = document.getElementById('lowercase-btn'); | |
| const uppercaseBtn = document.getElementById('uppercase-btn'); | |
| const boldBtn = document.getElementById('bold-btn'); | |
| const italicBtn = document.getElementById('italic-btn'); | |
| const underlineBtn = document.getElementById('underline-btn'); | |
| const capitalizeBtn = document.getElementById('capitalize-btn'); | |
| const reverseBtn = document.getElementById('reverse-btn'); | |
| const encodeBtn = document.getElementById('encode-btn'); | |
| const decodeBtn = document.getElementById('decode-btn'); | |
| const removeFormatBtn = document.getElementById('remove-format-btn'); | |
| // New feature buttons | |
| const findReplaceBtn = document.getElementById('find-replace-btn'); | |
| const analyzeBtn = document.getElementById('analyze-btn'); | |
| const speakToggleBtn = document.getElementById('speak-toggle-btn'); | |
| const findReplacePanel = document.getElementById('find-replace-panel'); | |
| const analysisPanel = document.getElementById('analysis-panel'); | |
| const speechPanel = document.getElementById('speech-panel'); | |
| // Find & Replace elements | |
| const findInput = document.getElementById('find-input'); | |
| const replaceInput = document.getElementById('replace-input'); | |
| const findNextBtn = document.getElementById('find-next-btn'); | |
| const findPrevBtn = document.getElementById('find-prev-btn'); | |
| const replaceBtn = document.getElementById('replace-btn'); | |
| const caseSensitiveCheckbox = document.getElementById('case-sensitive'); | |
| const findResults = document.getElementById('find-results'); | |
| // Analysis elements | |
| const analysisChars = document.getElementById('analysis-chars'); | |
| const analysisWords = document.getElementById('analysis-words'); | |
| const analysisSentences = document.getElementById('analysis-sentences'); | |
| const analysisReadingTime = document.getElementById('analysis-reading-time'); | |
| const wordFrequencyList = document.getElementById('word-frequency-list'); | |
| // Speech synthesis elements | |
| const voiceSelect = document.getElementById('voice-select'); | |
| const rateControl = document.getElementById('rate-control'); | |
| const pitchControl = document.getElementById('pitch-control'); | |
| const rateValue = document.getElementById('rate-value'); | |
| const pitchValue = document.getElementById('pitch-value'); | |
| const speakBtn = document.getElementById('speak-btn'); | |
| const pauseBtn = document.getElementById('pause-btn'); | |
| const stopBtn = document.getElementById('stop-btn'); | |
| const progressBarFill = document.getElementById('progress-bar-fill'); | |
| // Variables for find & replace | |
| let currentFindIndex = -1; | |
| let findMatches = []; | |
| // Variables for speech synthesis | |
| let speechSynthesis = window.speechSynthesis; | |
| let speechUtterance = null; | |
| let voices = []; | |
| let isSpeaking = false; | |
| let isPaused = false; | |
| let speechProgressInterval; | |
| // Play click sound | |
| function playClickSound() { | |
| clickSound.currentTime = 0; | |
| clickSound.play().catch(e => console.log("Audio play failed:", e)); | |
| } | |
| // Theme toggle | |
| function toggleTheme() { | |
| if (document.documentElement.classList.contains('dark')) { | |
| document.documentElement.classList.remove('dark'); | |
| localStorage.setItem('theme', 'light'); | |
| themeIcon.classList.remove('fa-sun'); | |
| themeIcon.classList.add('fa-moon'); | |
| } else { | |
| document.documentElement.classList.add('dark'); | |
| localStorage.setItem('theme', 'dark'); | |
| themeIcon.classList.remove('fa-moon'); | |
| themeIcon.classList.add('fa-sun'); | |
| } | |
| playClickSound(); | |
| } | |
| // Check for saved theme preference | |
| if (localStorage.getItem('theme') === 'dark' || | |
| (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) { | |
| document.documentElement.classList.add('dark'); | |
| themeIcon.classList.remove('fa-moon'); | |
| themeIcon.classList.add('fa-sun'); | |
| } | |
| themeToggle.addEventListener('click', toggleTheme); | |
| // Update character and word count | |
| function updateCounts() { | |
| const text = cleanTextArea.value; | |
| charCount.textContent = `${text.length} characters`; | |
| const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length; | |
| wordCount.textContent = `${words} words`; | |
| } | |
| // Handle paste from button | |
| pasteBtn.addEventListener('click', async function() { | |
| playClickSound(); | |
| try { | |
| const text = await navigator.clipboard.readText(); | |
| cleanTextArea.value = text; | |
| updateCounts(); | |
| // Show success feedback | |
| pasteBtn.innerHTML = '<i class="fas fa-check mr-1"></i> Pasted!'; | |
| pasteBtn.classList.remove('bg-blue-100', 'text-blue-700', 'dark:bg-blue-900', 'dark:text-blue-200'); | |
| pasteBtn.classList.add('bg-green-100', 'text-green-700', 'dark:bg-green-900', 'dark:text-green-200'); | |
| setTimeout(() => { | |
| pasteBtn.innerHTML = '<i class="fas fa-paste mr-1"></i> Paste'; | |
| pasteBtn.classList.remove('bg-green-100', 'text-green-700', 'dark:bg-green-900', 'dark:text-green-200'); | |
| pasteBtn.classList.add('bg-blue-100', 'text-blue-700', 'dark:bg-blue-900', 'dark:text-blue-200'); | |
| }, 1500); | |
| } catch (err) { | |
| alert('Failed to read clipboard. Please paste manually into the text area.'); | |
| console.error('Failed to read clipboard contents:', err); | |
| } | |
| }); | |
| // Handle copy button | |
| copyBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| if (cleanTextArea.value.trim() === '') { | |
| alert('Nothing to copy! Please paste some text first.'); | |
| return; | |
| } | |
| navigator.clipboard.writeText(cleanTextArea.value) | |
| .then(() => { | |
| // Show tooltip | |
| tooltip.classList.add('show-tooltip'); | |
| setTimeout(() => { | |
| tooltip.classList.remove('show-tooltip'); | |
| }, 2000); | |
| }) | |
| .catch(err => { | |
| console.error('Failed to copy text: ', err); | |
| alert('Failed to copy text. Please try again.'); | |
| }); | |
| }); | |
| // Handle clear button | |
| clearBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| cleanTextArea.value = ''; | |
| updateCounts(); | |
| clearHighlights(); | |
| findMatches = []; | |
| currentFindIndex = -1; | |
| findResults.textContent = '0 matches'; | |
| // Stop any ongoing speech | |
| stopSpeech(); | |
| }); | |
| // Formatting functions | |
| function applyFormat(formatFn) { | |
| if (cleanTextArea.value.trim() === '') { | |
| alert('No text to format! Please paste some text first.'); | |
| return; | |
| } | |
| const startPos = cleanTextArea.selectionStart; | |
| const endPos = cleanTextArea.selectionEnd; | |
| if (startPos === endPos) { | |
| // No selection - format entire text | |
| cleanTextArea.value = formatFn(cleanTextArea.value); | |
| } else { | |
| // Format only selected text | |
| const selectedText = cleanTextArea.value.substring(startPos, endPos); | |
| const formattedText = formatFn(selectedText); | |
| cleanTextArea.value = cleanTextArea.value.substring(0, startPos) + | |
| formattedText + | |
| cleanTextArea.value.substring(endPos); | |
| } | |
| updateCounts(); | |
| } | |
| // Base64 encode function | |
| function encodeBase64(text) { | |
| return btoa(unescape(encodeURIComponent(text))); | |
| } | |
| // Base64 decode function | |
| function decodeBase64(text) { | |
| try { | |
| return decodeURIComponent(escape(atob(text))); | |
| } catch (e) { | |
| alert("Invalid Base64 encoded string!"); | |
| return text; | |
| } | |
| } | |
| // Format button handlers | |
| lowercaseBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => text.toLowerCase()); | |
| highlightActiveButton(lowercaseBtn); | |
| }); | |
| uppercaseBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => text.toUpperCase()); | |
| highlightActiveButton(uppercaseBtn); | |
| }); | |
| boldBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => { | |
| const startPos = cleanTextArea.selectionStart; | |
| const endPos = cleanTextArea.selectionEnd; | |
| if (startPos === endPos) { | |
| return text.replace(/([^\n])([^\n]+)?/g, function(match, p1, p2) { | |
| return p1 + (p2 ? '**' + p2 + '**' : ''); | |
| }); | |
| } else { | |
| return '**' + text + '**'; | |
| } | |
| }); | |
| highlightActiveButton(boldBtn); | |
| }); | |
| italicBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => { | |
| const startPos = cleanTextArea.selectionStart; | |
| const endPos = cleanTextArea.selectionEnd; | |
| if (startPos === endPos) { | |
| return text.replace(/([^\n])([^\n]+)?/g, function(match, p1, p2) { | |
| return p1 + (p2 ? '_' + p2 + '_' : ''); | |
| }); | |
| } else { | |
| return '_' + text + '_'; | |
| } | |
| }); | |
| highlightActiveButton(italicBtn); | |
| }); | |
| underlineBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => { | |
| const startPos = cleanTextArea.selectionStart; | |
| const endPos = cleanTextArea.selectionEnd; | |
| if (startPos === endPos) { | |
| return text.replace(/([^\n])([^\n]+)?/g, function(match, p1, p2) { | |
| return p1 + (p2 ? '<u>' + p2 + '</u>' : ''); | |
| }); | |
| } else { | |
| return '<u>' + text + '</u>'; | |
| } | |
| }); | |
| highlightActiveButton(underlineBtn); | |
| }); | |
| capitalizeBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => { | |
| return text.toLowerCase().replace(/(^|\s)\S/g, function(firstLetter) { | |
| return firstLetter.toUpperCase(); | |
| }); | |
| }); | |
| highlightActiveButton(capitalizeBtn); | |
| }); | |
| reverseBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => { | |
| return text.split('').reverse().join(''); | |
| }); | |
| highlightActiveButton(reverseBtn); | |
| }); | |
| encodeBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => { | |
| return encodeBase64(text); | |
| }); | |
| highlightActiveButton(encodeBtn); | |
| }); | |
| decodeBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => { | |
| return decodeBase64(text); | |
| }); | |
| highlightActiveButton(decodeBtn); | |
| }); | |
| removeFormatBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| applyFormat(text => { | |
| // Remove markdown formatting | |
| let cleaned = text.replace(/(\*\*|__)(.*?)\1/g, '$2'); // bold | |
| cleaned = cleaned.replace(/(\*|_)(.*?)\1/g, '$2'); // italic | |
| cleaned = cleaned.replace(/<u>(.*?)<\/u>/g, '$1'); // underline | |
| cleaned = cleaned.replace(/<[^>]+>/g, ''); // any HTML tags | |
| return cleaned; | |
| }); | |
| highlightActiveButton(removeFormatBtn); | |
| }); | |
| // Highlight active format button | |
| function highlightActiveButton(button) { | |
| // Remove active class from all format buttons | |
| document.querySelectorAll('.format-btn').forEach(btn => { | |
| if (!btn.classList.contains('code-btn') && | |
| !btn.classList.contains('bg-purple-100') && | |
| !btn.classList.contains('bg-green-100') && | |
| !btn.classList.contains('bg-indigo-100')) { | |
| btn.classList.remove('active-format'); | |
| btn.classList.add('bg-gray-200', 'text-gray-700', 'dark:bg-slate-600', 'dark:text-gray-200'); | |
| btn.classList.remove('bg-blue-600', 'text-white'); | |
| } | |
| }); | |
| // Add active class to clicked button | |
| if (button.classList.contains('code-btn')) { | |
| button.classList.add('bg-yellow-600', 'dark:bg-yellow-700'); | |
| } else if (button.classList.contains('bg-purple-100')) { | |
| button.classList.add('bg-purple-600', 'text-white'); | |
| } else if (button.classList.contains('bg-green-100')) { | |
| button.classList.add('bg-green-600', 'text-white'); | |
| } else if (button.classList.contains('bg-indigo-100')) { | |
| button.classList.add('bg-indigo-600', 'text-white'); | |
| } else { | |
| button.classList.add('active-format'); | |
| button.classList.remove('bg-gray-200', 'text-gray-700', 'dark:bg-slate-600', 'dark:text-gray-200'); | |
| button.classList.add('bg-blue-600', 'text-white'); | |
| } | |
| // Remove highlight after 1.5 seconds | |
| setTimeout(() => { | |
| if (button.classList.contains('code-btn')) { | |
| button.classList.remove('bg-yellow-600', 'dark:bg-yellow-700'); | |
| } else if (button.classList.contains('bg-purple-100')) { | |
| button.classList.remove('bg-purple-600', 'text-white'); | |
| } else if (button.classList.contains('bg-green-100')) { | |
| button.classList.remove('bg-green-600', 'text-white'); | |
| } else if (button.classList.contains('bg-indigo-100')) { | |
| button.classList.remove('bg-indigo-600', 'text-white'); | |
| } else { | |
| button.classList.remove('active-format'); | |
| button.classList.remove('bg-blue-600', 'text-white'); | |
| button.classList.add('bg-gray-200', 'text-gray-700', 'dark:bg-slate-600', 'dark:text-gray-200'); | |
| } | |
| }, 1500); | |
| } | |
| // Toggle find & replace panel | |
| findReplaceBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| analysisPanel.classList.remove('open'); | |
| speechPanel.classList.remove('open'); | |
| findReplacePanel.classList.toggle('open'); | |
| if (findReplacePanel.classList.contains('open')) { | |
| highlightActiveButton(findReplaceBtn); | |
| findInput.focus(); | |
| } | |
| }); | |
| // Toggle analysis panel | |
| analyzeBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| findReplacePanel.classList.remove('open'); | |
| speechPanel.classList.remove('open'); | |
| analysisPanel.classList.toggle('open'); | |
| if (analysisPanel.classList.contains('open')) { | |
| highlightActiveButton(analyzeBtn); | |
| analyzeText(); | |
| } | |
| }); | |
| // Toggle speech panel | |
| speakToggleBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| findReplacePanel.classList.remove('open'); | |
| analysisPanel.classList.remove('open'); | |
| speechPanel.classList.toggle('open'); | |
| if (speechPanel.classList.contains('open')) { | |
| highlightActiveButton(speakToggleBtn); | |
| loadVoices(); | |
| } | |
| }); | |
| // Analyze text function | |
| function analyzeText() { | |
| const text = cleanTextArea.value; | |
| // Basic stats | |
| analysisChars.textContent = text.length; | |
| const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length; | |
| analysisWords.textContent = words; | |
| // Count sentences (very basic implementation) | |
| const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0).length; | |
| analysisSentences.textContent = sentences; | |
| // Calculate reading time (average reading speed: 200 words per minute) | |
| const readingTime = Math.ceil(words / 200); | |
| analysisReadingTime.textContent = readingTime <= 1 ? '1 min' : `${readingTime} mins`; | |
| // Word frequency analysis | |
| if (text.trim() === '') { | |
| wordFrequencyList.innerHTML = '<div class="text-center text-gray-500 dark:text-gray-400 py-2">No words to analyze</div>'; | |
| return; | |
| } | |
| // Process words | |
| const wordsArray = text.toLowerCase() | |
| .replace(/[^\w\s]/g, '') // Remove punctuation | |
| .split(/\s+/) | |
| .filter(word => word.length > 0); | |
| // Count word frequency | |
| const wordFrequency = {}; | |
| wordsArray.forEach(word => { | |
| wordFrequency[word] = (wordFrequency[word] || 0) + 1; | |
| }); | |
| // Sort by frequency | |
| const sortedWords = Object.keys(wordFrequency).sort((a, b) => wordFrequency[b] - wordFrequency[a]); | |
| // Display top 20 words | |
| wordFrequencyList.innerHTML = ''; | |
| sortedWords.slice(0, 20).forEach(word => { | |
| const frequency = wordFrequency[word]; | |
| const percentage = Math.round((frequency / wordsArray.length) * 100); | |
| const item = document.createElement('div'); | |
| item.className = 'word-frequency-item flex justify-between items-center mb-1 p-2 bg-white dark:bg-slate-600 rounded'; | |
| const wordSpan = document.createElement('span'); | |
| wordSpan.className = 'font-medium dark:text-white'; | |
| wordSpan.textContent = word; | |
| const freqSpan = document.createElement('span'); | |
| freqSpan.className = 'text-sm text-gray-500 dark:text-gray-300'; | |
| freqSpan.textContent = `${frequency} (${percentage}%)`; | |
| item.appendChild(wordSpan); | |
| item.appendChild(freqSpan); | |
| wordFrequencyList.appendChild(item); | |
| }); | |
| } | |
| // Find text function | |
| function findText(direction = 'next') { | |
| const searchText = findInput.value; | |
| if (!searchText) { | |
| alert('Please enter text to find'); | |
| return; | |
| } | |
| const text = cleanTextArea.value; | |
| const caseSensitive = caseSensitiveCheckbox.checked; | |
| const flags = caseSensitive ? 'g' : 'gi'; | |
| // Clear previous highlights | |
| clearHighlights(); | |
| // Find all matches | |
| const regex = new RegExp(escapeRegExp(searchText), flags); | |
| findMatches = []; | |
| let match; | |
| while ((match = regex.exec(text)) !== null) { | |
| findMatches.push({ | |
| start: match.index, | |
| end: match.index + match[0].length | |
| }); | |
| } | |
| findResults.textContent = `${findMatches.length} matches`; | |
| if (findMatches.length === 0) { | |
| alert('No matches found'); | |
| return; | |
| } | |
| // Navigate to next/previous match | |
| if (direction === 'next') { | |
| currentFindIndex = (currentFindIndex + 1) % findMatches.length; | |
| } else { | |
| currentFindIndex = (currentFindIndex - 1 + findMatches.length) % findMatches.length; | |
| } | |
| // Highlight all matches and scroll to current one | |
| highlightMatches(); | |
| // Scroll to current match | |
| const currentMatch = findMatches[currentFindIndex]; | |
| scrollToMatch(currentMatch.start, currentMatch.end); | |
| } | |
| // Helper function to escape regex special characters | |
| function escapeRegExp(string) { | |
| return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| } | |
| // Highlight all matches | |
| function highlightMatches() { | |
| const text = cleanTextArea.value; | |
| let highlightedText = ''; | |
| let lastIndex = 0; | |
| findMatches.forEach((match, index) => { | |
| // Add text before match | |
| highlightedText += text.substring(lastIndex, match.start); | |
| // Add highlighted match | |
| const matchClass = index === currentFindIndex ? 'current-highlight': 'highlight'; | |
| highlightedText += `<span class="${matchClass}">${text.substring(match.start, match.end)}</span>`; | |
| lastIndex = match.end; | |
| }); | |
| // Add remaining text | |
| highlightedText += text.substring(lastIndex); | |
| // Update textarea with highlighted HTML (using a hidden div) | |
| const hiddenDiv = document.createElement('div'); | |
| hiddenDiv.style.display = 'none'; | |
| hiddenDiv.innerHTML = highlightedText; | |
| document.body.appendChild(hiddenDiv); | |
| // Get plain text with markers for highlights | |
| let plainText = ''; | |
| const walker = document.createTreeWalker(hiddenDiv, NodeFilter.SHOW_TEXT, null, false); | |
| let node; | |
| while (node = walker.nextNode()) { | |
| plainText += node.nodeValue; | |
| } | |
| // Remove the hidden div | |
| document.body.removeChild(hiddenDiv); | |
| // Update textarea with plain text | |
| cleanTextArea.value = plainText; | |
| } | |
| // Clear all highlights | |
| function clearHighlights() { | |
| if (findMatches.length > 0) { | |
| const text = cleanTextArea.value; | |
| let plainText = ''; | |
| let lastIndex = 0; | |
| findMatches.forEach(match => { | |
| plainText += text.substring(lastIndex, match.start); | |
| plainText += text.substring(match.start, match.end); | |
| lastIndex = match.end; | |
| }); | |
| plainText += text.substring(lastIndex); | |
| cleanTextArea.value = plainText; | |
| findMatches = []; | |
| currentFindIndex = -1; | |
| findResults.textContent = '0 matches'; | |
| } | |
| } | |
| // Scroll to match position | |
| function scrollToMatch(start, end) { | |
| // Calculate line height (approximate) | |
| const lineHeight = 20; // px | |
| // Calculate number of lines to scroll | |
| const textBefore = cleanTextArea.value.substring(0, start); | |
| const linesBefore = textBefore.split('\n').length - 1; | |
| // Scroll to position | |
| cleanTextArea.scrollTop = linesBefore * lineHeight; | |
| // Set selection | |
| cleanTextArea.focus(); | |
| cleanTextArea.setSelectionRange(start, end); | |
| } | |
| // Replace text function | |
| function replaceText() { | |
| if (findMatches.length === 0) { | |
| findText(); | |
| return; | |
| } | |
| const replaceText = replaceInput.value; | |
| const currentMatch = findMatches[currentFindIndex]; | |
| // Replace current match | |
| const text = cleanTextArea.value; | |
| const newText = text.substring(0, currentMatch.start) + replaceText + text.substring(currentMatch.end); | |
| cleanTextArea.value = newText; | |
| // Adjust positions of remaining matches | |
| const lengthDiff = replaceText.length - (currentMatch.end - currentMatch.start); | |
| findMatches.forEach((match, index) => { | |
| if (match.start > currentMatch.start) { | |
| match.start += lengthDiff; | |
| match.end += lengthDiff; | |
| } | |
| }); | |
| // Remove current match from array | |
| findMatches.splice(currentFindIndex, 1); | |
| if (findMatches.length === 0) { | |
| findResults.textContent = '0 matches'; | |
| currentFindIndex = -1; | |
| return; | |
| } | |
| // Update current index and highlight | |
| currentFindIndex = Math.min(currentFindIndex, findMatches.length - 1); | |
| findResults.textContent = `${findMatches.length} matches`; | |
| highlightMatches(); | |
| } | |
| // Speech Synthesis Functions | |
| function loadVoices() { | |
| // Chrome loads voices asynchronously | |
| voices = speechSynthesis.getVoices(); | |
| voiceSelect.innerHTML = ''; | |
| if (voices.length === 0) { | |
| voiceSelect.innerHTML = '<option value="">Loading voices...</option>'; | |
| // Try again in 1 second if voices aren't loaded yet | |
| setTimeout(loadVoices, 1000); | |
| return; | |
| } | |
| // Filter voices to only include ones with localService (usually system voices) | |
| const systemVoices = voices.filter(voice => voice.localService); | |
| systemVoices.forEach((voice, index) => { | |
| const option = document.createElement('option'); | |
| option.value = index; | |
| option.textContent = `${voice.name} (${voice.lang})`; | |
| if (voice.default) { | |
| option.textContent += ' [Default]'; | |
| } | |
| voiceSelect.appendChild(option); | |
| }); | |
| // Try to select a reasonable default voice | |
| const defaultVoice = systemVoices.find(voice => | |
| voice.lang.startsWith('en-') && voice.default | |
| ) || systemVoices.find(voice => voice.default) || systemVoices[0]; | |
| if (defaultVoice) { | |
| const defaultIndex = systemVoices.indexOf(defaultVoice); | |
| voiceSelect.value = defaultIndex; | |
| } | |
| } | |
| function speakText() { | |
| if (isSpeaking) { | |
| pauseSpeech(); | |
| return; | |
| } | |
| if (isPaused) { | |
| resumeSpeech(); | |
| return; | |
| } | |
| const text = cleanTextArea.value; | |
| if (!text.trim()) { | |
| alert('No text to speak! Please paste some text first.'); | |
| return; | |
| } | |
| // Stop any ongoing speech | |
| stopSpeech(); | |
| // Create a new utterance | |
| speechUtterance = new SpeechSynthesisUtterance(text); | |
| // Set voice | |
| const selectedVoiceIndex = parseInt(voiceSelect.value); | |
| if (!isNaN(selectedVoiceIndex) && voices[selectedVoiceIndex]) { | |
| speechUtterance.voice = voices[selectedVoiceIndex]; | |
| } | |
| // Set rate and pitch | |
| speechUtterance.rate = parseFloat(rateControl.value); | |
| speechUtterance.pitch = parseFloat(pitchControl.value); | |
| // Update UI | |
| isSpeaking = true; | |
| speakBtn.innerHTML = '<i class="fas fa-pause mr-2"></i> Pause'; | |
| speakBtn.classList.remove('bg-green-500', 'hover:bg-green-600'); | |
| speakBtn.classList.add('bg-yellow-500', 'hover:bg-yellow-600'); | |
| speakToggleBtn.classList.add('voice-active'); | |
| // Reset progress bar | |
| progressBarFill.style.width = '0%'; | |
| // Start progress tracking | |
| const textLength = text.length; | |
| let currentPosition = 0; | |
| speechProgressInterval = setInterval(() => { | |
| if (speechSynthesis.speaking && !isPaused) { | |
| // Estimate position based on time elapsed | |
| const elapsed = (speechUtterance.elapsedTime || 0) * 1000; // ms | |
| const totalDuration = (textLength / (speechUtterance.rate * 8)) * 1000; // rough estimate | |
| const progress = Math.min(100, (elapsed / totalDuration) * 100); | |
| progressBarFill.style.width = `${progress}%`; | |
| } | |
| }, 100); | |
| // Event handlers | |
| speechUtterance.onend = function() { | |
| stopSpeech(); | |
| }; | |
| speechUtterance.onerror = function(event) { | |
| console.error('SpeechSynthesis error:', event); | |
| stopSpeech(); | |
| alert('Error occurred during speech synthesis: ' + event.error); | |
| }; | |
| speechUtterance.onboundary = function(event) { | |
| if (event.name === 'word') { | |
| currentPosition = event.charIndex; | |
| } | |
| }; | |
| // Speak the text | |
| speechSynthesis.speak(speechUtterance); | |
| } | |
| function pauseSpeech() { | |
| if (isSpeaking && !isPaused) { | |
| speechSynthesis.pause(); | |
| isSpeaking = false; | |
| isPaused = true; | |
| speakBtn.innerHTML = '<i class="fas fa-play mr-2"></i> Resume'; | |
| speakBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600'); | |
| speakBtn.classList.add('bg-green-500', 'hover:bg-green-600'); | |
| } | |
| } | |
| function resumeSpeech() { | |
| if (isPaused) { | |
| speechSynthesis.resume(); | |
| isSpeaking = true; | |
| isPaused = false; | |
| speakBtn.innerHTML = '<i class="fas fa-pause mr-2"></i> Pause'; | |
| speakBtn.classList.remove('bg-green-500', 'hover:bg-green-600'); | |
| speakBtn.classList.add('bg-yellow-500', 'hover:bg-yellow-600'); | |
| } | |
| } | |
| function stopSpeech() { | |
| speechSynthesis.cancel(); | |
| isSpeaking = false; | |
| isPaused = false; | |
| // Clear interval | |
| if (speechProgressInterval) { | |
| clearInterval(speechProgressInterval); | |
| speechProgressInterval = null; | |
| } | |
| // Reset UI | |
| speakBtn.innerHTML = '<i class="fas fa-play mr-2"></i> Speak'; | |
| speakBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600'); | |
| speakBtn.classList.add('bg-green-500', 'hover:bg-green-600'); | |
| speakToggleBtn.classList.remove('voice-active'); | |
| progressBarFill.style.width = '0%'; | |
| } | |
| // Initialize speech synthesis | |
| if ('speechSynthesis' in window) { | |
| // Chrome loads voices asynchronously | |
| speechSynthesis.onvoiceschanged = loadVoices; | |
| // Load voices immediately if they're already available | |
| if (speechSynthesis.getVoices().length > 0) { | |
| loadVoices(); | |
| } | |
| } else { | |
| // Disable speech features if not supported | |
| speakToggleBtn.disabled = true; | |
| speakToggleBtn.title = 'Text-to-speech not supported in your browser'; | |
| } | |
| // Find & Replace event listeners | |
| findNextBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| findText('next'); | |
| }); | |
| findPrevBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| findText('prev'); | |
| }); | |
| replaceBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| replaceText(); | |
| }); | |
| findInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter') { | |
| playClickSound(); | |
| findText('next'); | |
| } | |
| }); | |
| // Speech synthesis event listeners | |
| speakBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| speakText(); | |
| }); | |
| pauseBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| pauseSpeech(); | |
| }); | |
| stopBtn.addEventListener('click', function() { | |
| playClickSound(); | |
| stopSpeech(); | |
| }); | |
| rateControl.addEventListener('input', function() { | |
| rateValue.textContent = this.value; | |
| if (speechUtterance) { | |
| speechUtterance.rate = parseFloat(this.value); | |
| } | |
| }); | |
| pitchControl.addEventListener('input', function() { | |
| pitchValue.textContent = this.value; | |
| if (speechUtterance) { | |
| speechUtterance.pitch = parseFloat(this.value); | |
| } | |
| }); | |
| // Update counts when typing | |
| cleanTextArea.addEventListener('input', function() { | |
| updateCounts(); | |
| // Clear find highlights if text changes | |
| if (findMatches.length > 0) { | |
| clearHighlights(); | |
| } | |
| // Stop speech if text changes while speaking | |
| if (isSpeaking || isPaused) { | |
| stopSpeech(); | |
| } | |
| }); | |
| // Handle direct paste into textarea | |
| cleanTextArea.addEventListener('paste', function(e) { | |
| playClickSound(); | |
| // Let the paste happen first | |
| setTimeout(() => { | |
| updateCounts(); | |
| // Show feedback | |
| const originalPlaceholder = cleanTextArea.placeholder; | |
| cleanTextArea.placeholder = "✓ Text pasted!"; | |
| setTimeout(() => { | |
| cleanTextArea.placeholder = originalPlaceholder; | |
| }, 1500); | |
| }, 10); | |
| }); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', function(e) { | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'v' && document.activeElement !== cleanTextArea) { | |
| e.preventDefault(); | |
| pasteBtn.click(); | |
| } | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'c' && document.activeElement === cleanTextArea) { | |
| e.preventDefault(); | |
| copyBtn.click(); | |
| } | |
| // Formatting shortcuts | |
| if ((e.ctrlKey || e.metaKey) && document.activeElement === cleanTextArea) { | |
| switch(e.key.toLowerCase()) { | |
| case 'b': | |
| e.preventDefault(); | |
| boldBtn.click(); | |
| break; | |
| case 'i': | |
| e.preventDefault(); | |
| italicBtn.click(); | |
| break; | |
| case 'u': | |
| e.preventDefault(); | |
| underlineBtn.click(); | |
| break; | |
| case 'l': | |
| e.preventDefault(); | |
| lowercaseBtn.click(); | |
| break; | |
| case 'u': | |
| if (e.shiftKey) { | |
| e.preventDefault(); | |
| uppercaseBtn.click(); | |
| } | |
| break; | |
| case 'r': | |
| e.preventDefault(); | |
| reverseBtn.click(); | |
| break; | |
| case 'e': | |
| e.preventDefault(); | |
| encodeBtn.click(); | |
| break; | |
| case 'd': | |
| e.preventDefault(); | |
| decodeBtn.click(); | |
| break; | |
| case 'f': | |
| e.preventDefault(); | |
| findReplaceBtn.click(); | |
| break; | |
| case 'h': | |
| e.preventDefault(); | |
| analyzeBtn.click(); | |
| break; | |
| case 's': | |
| e.preventDefault(); | |
| speakToggleBtn.click(); | |
| break; | |
| } | |
| } | |
| // Find next/previous with F3/Shift+F3 | |
| if (e.key === 'F3') { | |
| e.preventDefault(); | |
| if (findReplacePanel.classList.contains('open')) { | |
| if (e.shiftKey) { | |
| findPrevBtn.click(); | |
| } else { | |
| findNextBtn.click(); | |
| } | |
| } | |
| } | |
| }); | |
| // Initialize counts | |
| updateCounts(); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=C50BARZ/clean-paste-10" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |