diff --git a/data/lessons.json b/data/lessons.json index 4dc7473..f592a90 100644 --- a/data/lessons.json +++ b/data/lessons.json @@ -2,17 +2,17 @@ { "lesson": 1, "title": "Grundreihe", - "text": "asdf asdf jklö asdf jklö as df jk lö\nasdf jklö asdf jklö as df jk lö fj as df jk lö\nasdf asdf jklö jklö asdf jklö as df jk lö asdf jklö" + "text": "asdf asdf jklö asdf jklö as df jk lö asdf jklö asdf jklö as df jk lö fj as df jk lö asdf asdf jklö jklö asdf jklö as df jk lö asdf jklö" }, { "lesson": 2, "title": "Kombinationen der Grundreihe", - "text": "as ja sa lö ölä da fj as ja sa lö ölä da fj\nkalal säda jöla aföl fall eska jass lösa dajf ölja lasa alla ösad fjöl\nfjöl assa lösa dajf salf jäda löfa söja läsa jäla fjod ölsa" + "text": "as ja sa lö ölä da fj as ja sa lö ölä da fj kalal säda jöla aföl fall eska jass lösa dajf ölja lasa alla ösad fjöl fjöl assa lösa dajf salf jäda löfa söja läsa jäla fjod ölsa" }, { "lesson": 3, "title": "Leichte Wörter", - "text": "Affe Tasche Leute Löwe Fels Saft Jäger Löcher Fassade Tasse Kaffee\nLöffel Straße Klasse Flosse Raster Sofa Falter Salz Wasser Lager Ölzug Liste Hälfte Saal Krater\nJalousie Lösung Läufer Förster Trage Latte Falle Löffel Jalousie Zähler Torte Hölle Futter Hülse" + "text": "Affe Tasche Leute Löwe Fels Saft Jäger Löcher Fassade Tasse Kaffee Löffel Straße Klasse Flosse Raster Sofa Falter Salz Wasser Lager Ölzug Liste Hälfte Saal Krater Jalousie Lösung Läufer Förster Trage Latte Falle Löffel Jalousie Zähler Torte Hölle Futter Hülse" }, { "lesson": 4, @@ -103,6 +103,16 @@ "lesson": 21, "title": "Kurze Test Lektion", "text": "Dies ist nur ein kurzer Text zum Testen." + }, + { + "lesson": 22, + "title": "Extra lange Test Lektion", + "text": "Lagebild des Bundeskriminalamts\nBedrohung durch Organisierte Kriminalität bleibt hoch\n\nDrogenhandel, Geldwäsche, Cybercrime - das Ausmaß der Organisierten Kriminalität in Deutschland bleibt laut BKA auf hohem Niveau. Sorge bereitet Ermittlern, dass Kriminelle zunehmend versuchen, Minderjährige für Straftaten zu rekrutieren. Die Bedrohung durch die Organisierte Kriminalität bleibt in Deutschland auf anhaltend hohem Niveau. Das geht aus dem Lagebild für das vergangene Jahr hervor, welches das Bundeskriminalamt (BKA) veröffentlicht hat. Demnach wurde 2024 in insgesamt 647 Verfahren wegen Organisierter Kriminalität ermittelt. Die Zahl bleibt im Vergleich zum Vorjahr nahezu gleich, 2024 stieg sie um fünf Verfahren an. Allerdings ist es der zweithöchste Wert innerhalb von zehn Jahren. Am häufigsten wurde laut BKA in Fällen von Rauschgiftkriminalität ermittelt - mit fast 260 Verfahren machte dieser Bereich den Großteil der Taten der Organisierten Kriminalität aus. Es folgten Fälle von Wirtschaftskriminalität mit etwa 110 Verfahren, Steuer- und Zolldelikte mit rund 60 Verfahren. Ähnlich viele Verfahren wurden wegen Schleuser- und Eigentumskriminalität verfolgt.Einen deutlich geringeren Anteil machten mit vier Prozent aller Verfahren Ermittlungen gegen Cybercrime-Gruppierungen aus. Allerdings ist der durch diese Vergehen entstandene Schaden massiv: Dem Lagebild zufolge wird er auf rund 1,7 Milliarden Euro geschätzt, was mehr als die Hälfte aller durch Organisierte Kriminalität verursachten finanziellen Schäden ausmacht. Insgesamt bezifferte das BKA die Finanzschäden im vergangenen Jahr auf etwa 2,64 Milliarden Euro." + }, + { + "lesson": 23, + "title": "Dummy: Leere Lektion", + "text": "Fast leer" } ] diff --git a/static/css/style.css b/static/css/style.css index d41562f..260c600 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -130,16 +130,37 @@ body { border-radius: 10px 10px 0 0 !important; } -#target-text { - min-height: 100px; - font-size: 1.2rem; - font-weight: 500; - line-height: 1.6; - letter-spacing: 0.5px; - user-select: none; /* Verhindert Textauswahl */ - -webkit-user-select: none; /* Für Safari */ - -moz-user-select: none; /* Für Firefox */ - -ms-user-select: none; /* Für Internet Explorer/Edge */ +#target-text { + min-height: 100px; + font-size: 1.2rem; + font-weight: 500; + line-height: 1.6; + letter-spacing: 0.5px; + user-select: none; /* Verhindert Textauswahl */ + -webkit-user-select: none; /* Für Safari */ + -moz-user-select: none; /* Für Firefox */ + -ms-user-select: none; /* Für Internet Explorer/Edge */ + white-space: pre-wrap; + overflow: auto; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: normal; +} + +.text-line { + display: block; + width: 100%; + white-space: pre-wrap; + font-family: monospace; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: normal; + hyphens: none; + max-width: 80ch; +} + +.highlight-neutral { + color: inherit; } #user-input { diff --git a/static/js/script.js b/static/js/script.js index dcf1abc..0d02aea 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -31,7 +31,7 @@ document.addEventListener('DOMContentLoaded', function() { initializeTheme(); document.getElementById('theme-toggle').addEventListener('click', toggleTheme); - const userInput = document.getElementById('user-input'); + const userInputElement = document.getElementById('user-input'); const targetTextElement = document.getElementById('target-text'); const nextBtn = document.getElementById('next-btn'); const endBtn = document.getElementById('end-btn'); @@ -45,17 +45,32 @@ document.addEventListener('DOMContentLoaded', function() { const wpmElement = document.getElementById('wpm'); // Aktuelle Textdaten - let lines = window.lines; - let currentLineIndex = window.currentLineIndex; + let fullText = window.fullText; + let currentCharIndex = window.currentCharIndex; let startTime = null; let timerInterval = null; - let completedLines = []; // Abgeschlossene Zeilen - let currentLineInput = ''; // Aktuelle Eingabe in der aktuellen Zeile - let fullText = lines.join('\n'); // Gesamter Text der Lektion + let userInput = ''; // Gesamte Eingabe let keyStrokeCount = 0; // Zähler für alle Tastaturanschläge (inkl. Backspace) + let lines = fullText.split('\n'); - // Zeige die erste Zeile an - updateLineDisplay(); + // Berechne die aktuelle Zeile basierend auf currentCharIndex + function getCurrentLineIndex() { + let total = 0; + for (let i = 0; i < lines.length; i++) { + let lineLength = lines[i].length; + if (i < lines.length - 1) { + lineLength += 1; // Für den Zeilenumbruch + } + if (currentCharIndex < total + lineLength) { + return i; + } + total += lineLength; + } + return lines.length - 1; + } + + // Zeige den Text an + updateTextDisplay(); // Verhindere Copy & Paste für das Vorlagenfeld targetTextElement.addEventListener('copy', function(event) { @@ -74,38 +89,38 @@ document.addEventListener('DOMContentLoaded', function() { }); // Fokus auf das Eingabefeld setzen - userInput.focus(); + userInputElement.focus(); // Verhindere Copy & Paste für das Eingabefeld - userInput.addEventListener('paste', function(event) { + userInputElement.addEventListener('paste', function(event) { event.preventDefault(); alert('Einfügen (Paste) ist nicht erlaubt! Bitte tippen Sie den Text manuell.'); }); - userInput.addEventListener('copy', function(event) { + userInputElement.addEventListener('copy', function(event) { event.preventDefault(); alert('Kopieren (Copy) ist nicht erlaubt!'); }); - userInput.addEventListener('cut', function(event) { + userInputElement.addEventListener('cut', function(event) { event.preventDefault(); alert('Ausschneiden (Cut) ist nicht erlaubt!'); }); // Verhindere Drag & Drop - userInput.addEventListener('drop', function(event) { + userInputElement.addEventListener('drop', function(event) { event.preventDefault(); alert('Drag & Drop ist nicht erlaubt! Bitte tippen Sie den Text manuell.'); }); // Verhindere das Einfügen über das Kontextmenü (rechte Maustaste) - userInput.addEventListener('contextmenu', function(event) { + userInputElement.addEventListener('contextmenu', function(event) { event.preventDefault(); return false; }); // Verhindere das Einfügen über Tastenkombinationen (Strg+V, Cmd+V, etc.) - userInput.addEventListener('keydown', function(event) { + userInputElement.addEventListener('keydown', function(event) { // Strg+V oder Cmd+V if ((event.ctrlKey || event.metaKey) && event.key === 'v') { event.preventDefault(); @@ -142,9 +157,9 @@ document.addEventListener('DOMContentLoaded', function() { }); // Starte die Zeitmessung beim ersten Tastendruck und zähle Tastaturanschläge - userInput.addEventListener('keydown', function(event) { - const cursorPosition = userInput.selectionStart; - const textLength = userInput.value.length; + userInputElement.addEventListener('keydown', function(event) { + const cursorPosition = userInputElement.selectionStart; + const textLength = userInputElement.value.length; let shouldCountKey = false; // Zähle Tastaturanschläge für alle Tasten, die keine Steuertasten sind (inkl. Backspace) @@ -174,55 +189,45 @@ document.addEventListener('DOMContentLoaded', function() { }); // Aktualisiere die Statistik bei jedem Tastendruck (keyup) - userInput.addEventListener('keyup', function() { - currentLineInput = this.value; - const currentLine = lines[currentLineIndex]; + userInputElement.addEventListener('keyup', function() { + userInput = this.value; + currentCharIndex = userInput.length; - // Wenn die Zeile vollständig korrekt getippt wurde, zur nächsten Zeile - if (currentLineInput === currentLine) { - if (currentLineIndex < lines.length - 1) { - completedLines.push(currentLineInput); - currentLineInput = ''; - this.value = ''; - currentLineIndex++; - updateLineDisplay(); - } else { - // Letzte Zeile abgeschlossen - Lektion beendet - completedLines.push(currentLineInput); - currentLineInput = ''; - this.value = ''; - - // Speichere die Statistiken für diese Lektion - const lessonIndex = window.currentLessonIndex; - const charsPerMinute = parseInt(speedElement.textContent); - const errorRate = parseFloat(errorRateElement.textContent); - const wpm = parseInt(wpmElement.textContent); - - fetch('/save_lesson_statistics', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - lesson_index: lessonIndex, - chars_per_minute: charsPerMinute, - error_rate: errorRate, - wpm: wpm - }) + // Aktualisiere die Anzeige bei jeder Änderung + updateTextDisplay(); + + // Wenn die gesamte Lektion abgeschlossen ist + if (currentCharIndex === fullText.length) { + // Speichere die Statistiken für diese Lektion + const lessonIndex = window.currentLessonIndex; + const charsPerMinute = parseInt(speedElement.textContent); + const errorRate = parseFloat(errorRateElement.textContent); + const wpm = parseInt(wpmElement.textContent); + + fetch('/save_lesson_statistics', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + lesson_index: lessonIndex, + chars_per_minute: charsPerMinute, + error_rate: errorRate, + wpm: wpm }) - .then(response => response.json()) - .then(data => { - if (data.status === 'success') { - alert('Lektion beendet! Statistiken wurden gespeichert.'); - } else { - alert('Lektion beendet, aber Statistiken konnten nicht gespeichert werden.'); - } - }) - .catch(error => { - console.error('Fehler beim Speichern der Statistiken:', error); - alert('Lektion beendet! (Fehler beim Speichern der Statistiken)'); - }); - } + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + alert('Lektion beendet! Statistiken wurden gespeichert.'); + } else { + alert('Lektion beendet, aber Statistiken konnten nicht gespeichert werden.'); + } + }) + .catch(error => { + console.error('Fehler beim Speichern der Statistiken:', error); + alert('Lektion beendet! (Fehler beim Speichern der Statistiken)'); + }); } updateStatistics(); @@ -267,17 +272,16 @@ document.addEventListener('DOMContentLoaded', function() { if (data.error) { alert('Fehler: ' + data.error); } else { - lines = data.text.split(/\r?\n/); - currentLineIndex = 0; - completedLines = []; - currentLineInput = ''; fullText = data.text; + currentCharIndex = 0; + userInput = ''; startTime = null; keyStrokeCount = 0; // Zurücksetzen des Tastaturanschlagszählers - updateLineDisplay(); + lines = fullText.split('\n'); + updateTextDisplay(); document.getElementById('lesson-title').textContent = data.title; - userInput.value = ''; - userInput.focus(); + userInputElement.value = ''; + userInputElement.focus(); updateStatistics(); // Aktualisiere die Markierung der aktuellen Lektion updateLessonHighlight(lessonIndex); @@ -304,18 +308,17 @@ document.addEventListener('DOMContentLoaded', function() { }) .then(response => response.json()) .then(data => { - lines = data.text.split(/\r?\n/); - currentLineIndex = 0; - completedLines = []; - currentLineInput = ''; fullText = data.text; + currentCharIndex = 0; + userInput = ''; startTime = null; keyStrokeCount = 0; // Zurücksetzen des Tastaturanschlagszählers - updateLineDisplay(); + lines = fullText.split('\n'); + updateTextDisplay(); // Aktualisiere den Lektionstitel document.getElementById('lesson-title').textContent = data.title; - userInput.value = ''; - userInput.focus(); // Fokus zurück auf Eingabefeld + userInputElement.value = ''; + userInputElement.focus(); // Fokus zurück auf Eingabefeld updateStatistics(); // Aktualisiere die Markierung der aktuellen Lektion updateLessonHighlight(data.index); @@ -349,21 +352,13 @@ document.addEventListener('DOMContentLoaded', function() { elapsedTime = (now - startTime) / 1000; // in Sekunden } - // Berechne die gesamte Benutzereingabe - let fullUserInput = completedLines.join('\n'); - if (completedLines.length > 0) { - fullUserInput += '\n' + currentLineInput; - } else { - fullUserInput = currentLineInput; - } - // Berechne Statistiken let totalKeyStrokes = keyStrokeCount; // Verwende jetzt die Anzahl aller Tastaturanschläge (inkl. Backspace) let correctKeyStrokes = 0; // Zähle korrekte Zeichen - for (let i = 0; i < fullUserInput.length; i++) { - if (i < fullText.length && fullUserInput[i] === fullText[i]) { + for (let i = 0; i < userInput.length; i++) { + if (i < fullText.length && userInput[i] === fullText[i]) { correctKeyStrokes++; } else { break; @@ -384,63 +379,107 @@ document.addEventListener('DOMContentLoaded', function() { wpmElement.textContent = Math.round(wpm); // Aktualisiere die Textfarben - updateTextHighlighting(currentLineInput, lines[currentLineIndex]); + updateTextDisplay(); } - // Text hervorheben - function updateTextHighlighting(userInput, currentText) { - let highlightedText = ''; + // Hilfsfunktion zum Hervorheben eines Zeichens + function highlightChar(char, type) { + if (char === ' ') { + return `·`; + } else if (char === '\t') { + return ``; + } else if (char === '\n') { + return '
'; + } else { + return `${char}`; + } + } + + // Text hervorheben mit natürlichen Zeilenumbrüchen + function updateTextDisplay() { + const currentLineIndex = getCurrentLineIndex(); + let newHTML = ''; + + // Zeige 10 Zeilen ab der aktuellen Zeile an + const startLine = Math.max(0, currentLineIndex); + const endLine = Math.min(lines.length, startLine + 10); - // Verwende den ursprünglichen Text für die Anzeige - const originalText = currentText; - - for (let i = 0; i < originalText.length; i++) { - const currentChar = originalText[i]; - if (i < userInput.length) { - if (userInput[i] === currentChar) { - // Für korrekt getippte Zeichen - if (currentChar === ' ') { - highlightedText += '·'; - } else if (currentChar === '\t') { - highlightedText += ''; - } else if (currentChar === '\n') { - highlightedText += '
'; - } else { - highlightedText += `${currentChar}`; - } - } else { - // Für falsch getippte Zeichen - if (currentChar === ' ') { - highlightedText += '·'; - } else if (currentChar === '\t') { - highlightedText += ''; - } else if (currentChar === '\n') { - highlightedText += '
'; - } else { - highlightedText += `${currentChar}`; - } - } - } else { - // Für nicht eingegebene Zeichen - if (currentChar === ' ') { - highlightedText += '·'; - } else if (currentChar === '\t') { - highlightedText += ''; - } else if (currentChar === '\n') { - highlightedText += '
'; - } else { - highlightedText += currentChar; - } - } + // Berechne die Startposition jeder Zeile im Gesamttext + let lineStarts = []; + let currentStart = 0; + for (let i = 0; i < lines.length; i++) { + lineStarts[i] = currentStart; + currentStart += lines[i].length + 1; // +1 für den Zeilenumbruch } - targetTextElement.innerHTML = highlightedText; + for (let j = startLine; j < endLine; j++) { + const line = lines[j]; + const lineStart = lineStarts[j]; + const lineEnd = lineStart + line.length; + + let highlightedLine = ''; + for (let i = 0; i < line.length; i++) { + const currentChar = line[i]; + const absoluteIndex = lineStart + i; + if (absoluteIndex < userInput.length) { + if (userInput[absoluteIndex] === currentChar) { + highlightedLine += highlightChar(currentChar, 'correct'); + } else { + highlightedLine += highlightChar(currentChar, 'incorrect'); + } + } else { + highlightedLine += highlightChar(currentChar, 'neutral'); + } + } + + // Füge den Zeilenumbruch nach jeder Zeile hinzu (außer der letzten) + if (j < lines.length - 1) { + const newlinePos = lineStart + line.length; + const newlineChar = '\n'; + if (newlinePos < userInput.length) { + if (userInput[newlinePos] === newlineChar) { + highlightedLine += highlightChar(newlineChar, 'correct'); + } else { + highlightedLine += highlightChar(newlineChar, 'incorrect'); + } + } else { + highlightedLine += highlightChar(newlineChar, 'neutral'); + } + } + + // Füge die Zeile als div hinzu, markiere die aktuelle Zeile + newHTML += `
${highlightedLine}
`; + } + + targetTextElement.innerHTML = newHTML; + + // Scrolle zur aktuellen Zeile (oben) + const currentLineElement = targetTextElement.querySelector('.current-line'); + if (currentLineElement) { + currentLineElement.scrollIntoView({ block: 'start', behavior: 'smooth' }); + } } - - // Hilfsfunktion zum Aktualisieren der Zeilenanzeige - function updateLineDisplay() { - // Setze nur die aktuelle Zeile mit Hervorhebung - updateTextHighlighting('', lines[currentLineIndex]); + + // Funktion zum Überprüfen und Scrollen, wenn die oberste Zeile komplett korrekt ist + function checkAndScrollTopLine() { + // Wenn wir nicht mehr in der ersten Zeile sind und die oberste Zeile in der Anzeige komplett grün ist + if (currentLineIndex > 0) { + // Die oberste angezeigte Zeile ist die Zeile bei currentLineIndex + // Da wir jetzt alle Zeilen ab currentLineIndex anzeigen, müssen wir prüfen, ob die erste Zeile (currentLineIndex) komplett grün ist? + // Tatsächlich: Die oberste Zeile in der Anzeige ist die Zeile bei currentLineIndex. + // Aber wir haben die completedLines, die die abgeschlossenen Zeilen speichert. + // Stattdessen: Wir können prüfen, ob die aktuelle Eingabe für die aktuelle Zeile mindestens so lang ist wie die Zeile selbst? + // Das ist bereits in der keyup-Event-Behandlung für den Zeilenwechsel erledigt. + // Allerdings: Wir wollen scrollen, wenn der Benutzer die oberste Zeile in der Anzeige (die bereits abgeschlossen ist) verlässt. + // Da wir die completedLines haben, wissen wir, dass alle Zeilen vor currentLineIndex abgeschlossen sind. + // Daher: Einfach nach jedem Tastendruck scrollen, wenn wir nicht in der ersten Zeile sind? + // Das könnte zu viel sein. Stattdessen: Scrollen, wenn die aktuelle Zeile nicht die erste ist. + // Aber: Wir haben bereits beim Zeilenwechsel gescrollt. Also vielleicht reicht das? + // Der Benutzer wünscht: "Wenn die oberste Zeile des angezeigten Textes in der Vorlage korrekt eingegeben worden ist (grün ist), soll das Programm die Anzeige automatisch um eine Zeile herunter scrollen." + // Das bedeutet: Sobald eine Zeile abgeschlossen ist und wir zur nächsten Zeile wechseln, soll die Anzeige so scrollen, dass die nächste Zeile sichtbar wird. + // Da wir jetzt eine feste Höhe haben und alle verbleibenden Zeilen anzeigen, müssen wir beim Wechsel zur nächsten Zeile scrollen. + // Dies wird bereits im keyup-Event nach dem Zeilenwechsel erledigt. + } } // Event-Listener für Statistik-Link (navigiert zur Statistik-Seite) @@ -489,8 +528,8 @@ document.addEventListener('DOMContentLoaded', function() { } // Initialisiere Statistik und Textfarben - updateLineDisplay(); - updateStatistics('', lines[currentLineIndex]); + updateTextDisplay(); + updateStatistics(); // Initialisiere Toggle-Funktionalität initializeProgressStatsToggle(); diff --git a/templates/index.html b/templates/index.html index 32daedd..e8cc9d5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -37,18 +37,16 @@ - +
Vorlage: {{ current_lesson_title }}
-
+