-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Add "Wait for space" and settings toggle #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
56a6b97
8e93e1c
bfb0613
52896da
8c7fd7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,8 +13,6 @@ document.addEventListener('DOMContentLoaded', () => { | |
| const newTextBtn = document.getElementById('new-text-btn'); | ||
| const deleteTextBtn = document.getElementById('delete-text-btn'); | ||
| const textDisplay = document.getElementById('text-display'); | ||
| const speakerIcon = document.getElementById('speaker-icon'); | ||
| const diffDisplay = document.getElementById('diff-display'); | ||
| const writingInput = document.getElementById('writing-input'); | ||
| const speedSlider = document.getElementById('speed-slider'); | ||
| const fontSizeSelect = document.getElementById('font-size-select'); | ||
|
|
@@ -25,14 +23,16 @@ document.addEventListener('DOMContentLoaded', () => { | |
| const ignoreAccentsCheckbox = document.getElementById('ignore-accents-checkbox'); | ||
| const ignorePunctuationCheckbox = document.getElementById('ignore-punctuation-checkbox'); | ||
| const ignoreCaseCheckbox = document.getElementById('ignore-case-checkbox'); | ||
| const waitForSpaceCheckbox = document.getElementById('wait-for-space-checkbox'); | ||
| const textTitleInput = document.getElementById('text-title-input'); | ||
| const textContentTextarea = document.getElementById('text-content-textarea'); | ||
| const saveTextBtn = document.getElementById('save-text-btn'); | ||
| const repeatWordBtn = document.getElementById('repeat-word-btn'); | ||
| const revealTextBtn = document.getElementById('reveal-text-btn'); | ||
| const toggleHiddenBtn = document.getElementById('toggle-hidden-btn'); | ||
| const resetSettingsBtn = document.getElementById('reset-settings-btn'); | ||
| const notificationArea = document.getElementById('notification-area'); | ||
| const configPanel = document.getElementById('config-panel'); | ||
| const menuToggleBtn = document.getElementById('menu-toggle-btn'); | ||
|
|
||
| // --- App State --- | ||
| let texts = {}; | ||
|
|
@@ -118,6 +118,11 @@ document.addEventListener('DOMContentLoaded', () => { | |
| return fragment; | ||
| }; | ||
|
|
||
| const obscureWord = (word) => { | ||
| // Replaces letters and numbers with black boxes, preserves punctuation. | ||
| return word.replace(/[\p{L}\p{N}]/gu, '■'); | ||
| }; | ||
|
|
||
| // ** Level 2: Dependencies on Level 1 ** | ||
| let isSpeaking = false; | ||
|
|
||
|
|
@@ -190,66 +195,72 @@ document.addEventListener('DOMContentLoaded', () => { | |
| speakImmediately(fullText, parseFloat(speedSlider.value)); | ||
| }; | ||
|
|
||
| const flashIncorrect = () => { | ||
| writingInput.classList.add('input-incorrect-flash'); | ||
| setTimeout(() => { | ||
| writingInput.classList.remove('input-incorrect-flash'); | ||
| }, 300); | ||
|
||
| }; | ||
|
|
||
| const handleContinuousInput = () => { | ||
| const sourceSpans = Array.from(textDisplay.querySelectorAll('span')); | ||
| const sourceSpans = Array.from(textDisplay.querySelectorAll('.word-span')); | ||
| const inputValue = writingInput.value; | ||
| const inputWords = inputValue.split(/[\s\n]+/).filter(w => w.length > 0); | ||
| const isInputComplete = /[\s\n]$/.test(inputValue) || inputValue.length === 0; | ||
| const inputTokens = inputValue.match(/\S+\s*/g) || []; | ||
| const isHiddenMode = hideTextCheckbox.checked; | ||
|
|
||
| let lastCorrectIndex = -1; | ||
| let firstErrorFound = null; | ||
|
|
||
| // Part 1: Determine correctness of each word and find the first error | ||
| // Part 1: Determine correctness of each word | ||
| sourceSpans.forEach(span => span.classList.remove('correct', 'incorrect', 'current')); | ||
|
|
||
| sourceSpans.forEach((span, index) => { | ||
| if (index < inputWords.length) { | ||
| const isLastWord = index === inputWords.length - 1; | ||
| if (index < inputTokens.length) { | ||
| const token = inputTokens[index]; | ||
| const inputWord = token.trim(); | ||
| const wordIsFinishedBySpace = token.length > inputWord.length; | ||
| const isLastToken = index === inputTokens.length - 1; | ||
|
|
||
| const normalizedSource = normalizeWord(sourceWords[index]); | ||
| const normalizedInput = normalizeWord(inputWords[index]); | ||
| const normalizedInput = normalizeWord(inputWord); | ||
|
|
||
| let isIncorrect = false; | ||
| if (isLastWord && !isInputComplete) { // Typing last word | ||
| if (!normalizedSource.startsWith(normalizedInput)) { | ||
| isIncorrect = true; | ||
| } | ||
| } else { // Word is complete | ||
| if (normalizedInput !== normalizedSource) { | ||
| isIncorrect = true; | ||
| } | ||
| const isWordInteractionComplete = !isLastToken || wordIsFinishedBySpace; | ||
|
|
||
| if (isWordInteractionComplete) { | ||
| if (normalizedInput !== normalizedSource) isIncorrect = true; | ||
| } else { | ||
| if (!normalizedSource.startsWith(normalizedInput)) isIncorrect = true; | ||
| } | ||
|
|
||
| if (isIncorrect) { | ||
| span.classList.add('incorrect'); | ||
| if (!firstErrorFound) { | ||
| firstErrorFound = { input: inputWords[index], source: sourceWords[index] }; | ||
| if (waitForSpaceCheckbox.checked && !isWordInteractionComplete) { | ||
| flashIncorrect(); | ||
| } else { | ||
| span.classList.add('incorrect'); | ||
| if (isHiddenMode) { | ||
| const diffHtml = createDiffHtml(inputWord, sourceWords[index]); | ||
| span.innerHTML = ''; // Clear the black boxes | ||
| span.appendChild(diffHtml); | ||
| } | ||
| } | ||
| } else { | ||
| // Only mark as correct if the word is complete | ||
| if (!isLastWord || isInputComplete) { | ||
| if (isWordInteractionComplete) { | ||
| span.classList.add('correct'); | ||
| if (isHiddenMode) { | ||
| // If it was previously incorrect (showing a diff) or just completed, | ||
| // reveal the correct word permanently. | ||
| span.textContent = sourceWords[index]; | ||
| } | ||
| lastCorrectIndex = index; | ||
| } | ||
| } | ||
| } else if (isHiddenMode) { | ||
| // For words not yet typed in hidden mode, ensure they are black boxes | ||
| span.textContent = obscureWord(sourceWords[index]); | ||
| } | ||
| }); | ||
|
|
||
| // Part 2: Update the hidden mode display based on whether an error was found | ||
| if (isHiddenMode) { | ||
| if (firstErrorFound) { | ||
| const diffHtml = createDiffHtml(firstErrorFound.input, firstErrorFound.source); | ||
| diffDisplay.innerHTML = ''; | ||
| diffDisplay.appendChild(diffHtml); | ||
| diffDisplay.classList.remove('hidden'); | ||
| speakerIcon.classList.add('hidden'); | ||
| } else { | ||
| // No errors found, show the speaker icon | ||
| diffDisplay.classList.add('hidden'); | ||
| speakerIcon.classList.remove('hidden'); | ||
| } | ||
| } | ||
|
|
||
| // Part 3: Update current word index, handle audio, and save session | ||
| // Part 2: Update current word index, handle audio, and save session | ||
| const newWordIndex = lastCorrectIndex + 1; | ||
| const hasAdvanced = newWordIndex > currentWordIndex; | ||
| currentWordIndex = newWordIndex; | ||
|
|
@@ -276,19 +287,26 @@ document.addEventListener('DOMContentLoaded', () => { | |
| }; | ||
|
|
||
| // ** Level 4: Dependencies on Level 3 ** | ||
| const renderText = () => { | ||
| if (!sourceWords.length) { | ||
| textDisplay.innerHTML = ''; | ||
| return; | ||
| } | ||
| const isHidden = hideTextCheckbox.checked; | ||
| // Render words, obscuring if in hidden mode. | ||
| const wordsToRender = isHidden ? sourceWords.map(obscureWord) : sourceWords; | ||
| textDisplay.innerHTML = wordsToRender.map(word => `<span class="word-span">${word}</span>`).join(' '); | ||
|
||
| }; | ||
|
|
||
| const displayText = async (savedInput = '') => { | ||
| const title = textSelect.value; | ||
| if (title && texts[title]) { | ||
| // Clear any previous diff display when loading new text | ||
| diffDisplay.innerHTML = ''; | ||
| diffDisplay.classList.add('hidden'); | ||
|
|
||
| sourceWords = texts[title].split(' ').filter(w => w.length > 0); | ||
| currentWordIndex = 0; | ||
| tabKeyPressCount = 0; | ||
| writingInput.value = savedInput; | ||
|
|
||
| textDisplay.innerHTML = sourceWords.map(word => `<span>${word}</span>`).join(' '); | ||
| renderText(); // Use the helper to render the initial text display | ||
|
|
||
| const lang = await detectLanguage(texts[title]); | ||
| textDisplay.lang = lang; | ||
|
|
@@ -327,19 +345,11 @@ document.addEventListener('DOMContentLoaded', () => { | |
| }; | ||
|
|
||
| const toggleHideText = () => { | ||
| const isHidden = hideTextCheckbox.checked; | ||
| textDisplay.classList.toggle('hidden', isHidden); | ||
| revealTextBtn.classList.toggle('hidden', !isHidden); | ||
| renderText(); // Re-render the text display based on the new checkbox state | ||
| handleContinuousInput(); // Re-apply styles and reveal any existing errors | ||
|
|
||
| // When hiding text, show the speaker icon and hide the diff display. | ||
| // When showing text, hide both. | ||
| speakerIcon.classList.toggle('hidden', !isHidden); | ||
| diffDisplay.classList.toggle('hidden', true); // Always hide diff on toggle | ||
|
|
||
| if (isHidden) { | ||
| if (hideTextCheckbox.checked) { | ||
| writingInput.focus(); | ||
| // We re-run input handler to check if a diff should be shown immediately | ||
| handleContinuousInput(); | ||
| speakNextWord(); | ||
| } | ||
| }; | ||
|
|
@@ -380,6 +390,7 @@ document.addEventListener('DOMContentLoaded', () => { | |
| ignoreAccents: ignoreAccentsCheckbox.checked, | ||
| ignorePunctuation: ignorePunctuationCheckbox.checked, | ||
| ignoreCase: ignoreCaseCheckbox.checked, | ||
| waitForSpace: waitForSpaceCheckbox.checked, | ||
| }; | ||
| await idb.set(DB_CONFIG_KEY, config); | ||
| }; | ||
|
|
@@ -395,6 +406,7 @@ document.addEventListener('DOMContentLoaded', () => { | |
| ignoreAccentsCheckbox.checked = config.ignoreAccents !== false; | ||
| ignorePunctuationCheckbox.checked = config.ignorePunctuation !== false; | ||
| ignoreCaseCheckbox.checked = config.ignoreCase !== false; | ||
| waitForSpaceCheckbox.checked = config.waitForSpace !== false; | ||
|
|
||
| applyConfig({ fontSize: fontSizeSelect.value, fontFamily: fontFamilySelect.value }); | ||
| toggleHideText(); | ||
|
|
@@ -471,6 +483,7 @@ document.addEventListener('DOMContentLoaded', () => { | |
| ignoreAccentsCheckbox.addEventListener('change', saveConfig); | ||
| ignorePunctuationCheckbox.addEventListener('change', saveConfig); | ||
| ignoreCaseCheckbox.addEventListener('change', saveConfig); | ||
| waitForSpaceCheckbox.addEventListener('change', saveConfig); | ||
|
||
|
|
||
| writingInput.addEventListener('keydown', (event) => { | ||
| if (event.key === 'Tab') { | ||
|
|
@@ -479,14 +492,6 @@ document.addEventListener('DOMContentLoaded', () => { | |
| } | ||
| }); | ||
|
|
||
| const revealHiddenText = () => { | ||
| if (hideTextCheckbox.checked) { | ||
| hideTextCheckbox.checked = false; | ||
| toggleHideText(); | ||
| saveConfig(); | ||
| } | ||
| }; | ||
|
|
||
| const toggleHiddenTextMode = () => { | ||
| hideTextCheckbox.checked = !hideTextCheckbox.checked; | ||
| toggleHideText(); | ||
|
|
@@ -497,17 +502,22 @@ document.addEventListener('DOMContentLoaded', () => { | |
| if (event.ctrlKey && event.key.toLowerCase() === 's') { | ||
| event.preventDefault(); | ||
| speakText(); | ||
| } else if (event.key === '`') { | ||
| revealHiddenText(); | ||
| } else if (event.key === 'Escape') { | ||
| event.preventDefault(); | ||
| toggleHiddenTextMode(); | ||
| } | ||
| }); | ||
|
|
||
| revealTextBtn.addEventListener('click', revealHiddenText); | ||
| toggleHiddenBtn.addEventListener('click', toggleHiddenTextMode); | ||
|
|
||
| menuToggleBtn.addEventListener('click', () => { | ||
| configPanel.classList.toggle('config-panel-visible'); | ||
| }); | ||
|
|
||
| writingInput.addEventListener('focus', () => { | ||
| configPanel.classList.remove('config-panel-visible'); | ||
| }); | ||
|
|
||
| const initializeApp = async () => { | ||
| await loadConfig(); | ||
| await loadTexts(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For better code organization, this new
obscureWordhelper function should be moved to the// ** Level 1: No Dependencies **section (around line 47). Since it's a pure function with no external dependencies from this file, placing it with other similar functions will improve code readability and maintainability.