diff --git a/HILFE.md b/HILFE.md
index c26f37f..6bedbe3 100644
--- a/HILFE.md
+++ b/HILFE.md
@@ -2,6 +2,8 @@
Willkommen beim Typewriter Trainer! Diese Hilfe erklärt alle Funktionen und gibt Tipps für effektives Tipptraining.
+
+
---
## 🎯 Was ist Typewriter Trainer?
diff --git a/VERSION b/VERSION
index 268b033..fb7a04c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-v0.3.0
+v0.4.0
diff --git a/alembic/versions/1106215c45af_add_training_duration_to_lessonstatistic.py b/alembic/versions/1106215c45af_add_training_duration_to_lessonstatistic.py
new file mode 100644
index 0000000..cbcca25
--- /dev/null
+++ b/alembic/versions/1106215c45af_add_training_duration_to_lessonstatistic.py
@@ -0,0 +1,32 @@
+"""Add training_duration to LessonStatistic
+
+Revision ID: 1106215c45af
+Revises: 5fcbe91de50b
+Create Date: 2025-10-28 16:32:19.853276
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '1106215c45af'
+down_revision: Union[str, Sequence[str], None] = '5fcbe91de50b'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ """Upgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('lesson_statistic', sa.Column('training_duration', sa.Float(), nullable=True))
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('lesson_statistic', 'training_duration')
+ # ### end Alembic commands ###
diff --git a/app.py b/app.py
index f528840..f05d90a 100644
--- a/app.py
+++ b/app.py
@@ -6,7 +6,7 @@ import csv
import secrets
from io import StringIO
from typing import Dict, List, Any, Optional
-from flask import Flask, render_template, request, jsonify, session, Response, make_response
+from flask import Flask, render_template, request, jsonify, session, Response, make_response, send_from_directory
from flask_wtf.csrf import CSRFProtect
from models import db, Progress, Statistic, LessonStatistic, DailyPractice, UserSettings
from datetime import datetime, date, timedelta
@@ -155,11 +155,19 @@ def load_lessons() -> None:
except json.JSONDecodeError as e2:
logger.error(f"Automatische Reparatur fehlgeschlagen: {e2}")
- logger.warning("Verwende Fallback-Lektionen")
+ logger.warning("Verwende Fallback-Lektionen und erstelle neue Lektionen-Datei")
lessons = FALLBACK_LESSONS
+ # Speichere Fallback-Lektionen für zukünftige Verwendung
+ with open(DATA_PATH, "w", encoding="utf-8") as f:
+ json.dump(lessons, f, indent=2, ensure_ascii=False)
+ logger.info(f"Fallback-Lektionen gespeichert in: {DATA_PATH}")
else:
- logger.warning(f"Datei {DATA_PATH} nicht gefunden. Verwende Fallback-Lektionen.")
+ logger.warning(f"Datei {DATA_PATH} nicht gefunden. Verwende Fallback-Lektionen und erstelle Datei.")
lessons = FALLBACK_LESSONS
+ # Speichere Fallback-Lektionen für zukünftige Verwendung
+ with open(DATA_PATH, "w", encoding="utf-8") as f:
+ json.dump(lessons, f, indent=2, ensure_ascii=False)
+ logger.info(f"Fallback-Lektionen gespeichert in: {DATA_PATH}")
except Exception as e:
logger.error(f"Unerwarteter Fehler beim Laden der Lektionen: {e}")
@@ -415,6 +423,7 @@ def train():
# Lade zusätzliche Zustandsdaten aus der Session
total_elapsed_time = session.get('total_elapsed_time', 0)
+ net_training_time = session.get('net_training_time', 0)
key_stroke_count = session.get('key_stroke_count', 0)
is_paused = session.get('is_paused', False)
@@ -423,6 +432,7 @@ def train():
current_position=cursor_position,
last_text=user_input,
total_elapsed_time=total_elapsed_time,
+ net_training_time=net_training_time,
key_stroke_count=key_stroke_count,
is_paused=is_paused,
lessons=lessons,
@@ -574,12 +584,13 @@ def update_progress():
elapsed_time = float(data.get('elapsed_time', 0))
cursor_position = int(data.get('cursor_position', len(user_input)))
total_elapsed_time = float(data.get('total_elapsed_time', 0))
+ net_training_time = float(data.get('net_training_time', 0))
key_stroke_count = int(data.get('key_stroke_count', 0))
except (ValueError, TypeError):
return jsonify({'error': 'Ungültige numerische Werte'}), 400
# Validiere Bereiche
- if elapsed_time < 0 or total_elapsed_time < 0 or key_stroke_count < 0:
+ if elapsed_time < 0 or total_elapsed_time < 0 or net_training_time < 0 or key_stroke_count < 0:
return jsonify({'error': 'Negative Werte sind nicht erlaubt'}), 400
if cursor_position < 0:
@@ -601,6 +612,7 @@ def update_progress():
# Speichere zusätzliche Zustandsdaten in der Session
session['total_elapsed_time'] = total_elapsed_time
+ session['net_training_time'] = net_training_time
session['key_stroke_count'] = key_stroke_count
session['is_paused'] = is_paused
@@ -754,6 +766,7 @@ def save_lesson_statistics():
chars_per_minute = float(data.get('chars_per_minute', 0))
error_rate = float(data.get('error_rate', 0))
wpm = float(data.get('wpm', 0))
+ training_duration = float(data.get('training_duration', 0))
except (ValueError, TypeError):
return jsonify({'error': 'Ungültige Datentypen'}), 400
@@ -761,13 +774,13 @@ def save_lesson_statistics():
if lesson_index < 0 or lesson_index >= len(lessons):
return jsonify({'error': 'Ungültiger Lektionsindex'}), 400
- if chars_per_minute < 0 or error_rate < 0 or wpm < 0:
+ if chars_per_minute < 0 or error_rate < 0 or wpm < 0 or training_duration < 0:
return jsonify({'error': 'Negative Werte sind nicht erlaubt'}), 400
if error_rate > 100:
return jsonify({'error': 'Fehlerrate kann nicht über 100% sein'}), 400
- logger.debug(f"Empfangene Statistik: lesson_index={lesson_index}, chars_per_minute={chars_per_minute}, error_rate={error_rate}, wpm={wpm}")
+ logger.debug(f"Empfangene Statistik: lesson_index={lesson_index}, chars_per_minute={chars_per_minute}, error_rate={error_rate}, wpm={wpm}, training_duration={training_duration}")
# Neue Statistik für die Lektion erstellen
try:
@@ -775,7 +788,8 @@ def save_lesson_statistics():
lesson_index=lesson_index,
chars_per_minute=chars_per_minute,
error_rate=error_rate,
- wpm=wpm
+ wpm=wpm,
+ training_duration=training_duration
)
db.session.add(lesson_stat)
@@ -835,7 +849,8 @@ def get_lesson_statistics(lesson_index: int) -> Response:
'dates': [stat.created_at.strftime('%Y-%m-%d') for stat in stats],
'chars_per_minute': [stat.chars_per_minute for stat in stats],
'error_rate': [stat.error_rate for stat in stats],
- 'wpm': [stat.wpm for stat in stats]
+ 'wpm': [stat.wpm for stat in stats],
+ 'training_duration': [stat.training_duration for stat in stats]
}
return jsonify(data)
@@ -956,6 +971,11 @@ def hilfe():
logger.error(f"Fehler beim Laden der Hilfe: {e}")
return render_template('hilfe.html', hilfe_html="
Hilfe konnte nicht geladen werden.
")
+@app.route('/images/')
+def serve_images(filename):
+ """Serviert Bilder aus dem images-Verzeichnis"""
+ return send_from_directory('images', filename)
+
@app.route('/settings')
def settings():
"""Einstellungsseite"""
@@ -1044,8 +1064,11 @@ def set_metronome():
except (ValueError, TypeError):
return jsonify({'error': 'Ungültiger BPM-Wert'}), 400
- if not (20 <= bpm <= 300):
- return jsonify({'error': 'BPM muss zwischen 20 und 300 liegen'}), 400
+ # Erweiterte Validierung mit detaillierten Fehlermeldungen
+ if bpm < 20:
+ return jsonify({'error': 'BPM muss mindestens 20 sein'}), 400
+ if bpm > 300:
+ return jsonify({'error': 'BPM darf maximal 300 sein'}), 400
sound = data.get('sound', 'beep')
if sound not in ['beep', 'click', 'woodblock']:
diff --git a/claude_start.txt b/claude_start_prompt.txt
similarity index 100%
rename from claude_start.txt
rename to claude_start_prompt.txt
diff --git a/images/Quertz_10_Finger_Layout.jpg b/images/Quertz_10_Finger_Layout.jpg
new file mode 100644
index 0000000..82d29ff
Binary files /dev/null and b/images/Quertz_10_Finger_Layout.jpg differ
diff --git a/models.py b/models.py
index 03de880..b46a108 100644
--- a/models.py
+++ b/models.py
@@ -26,6 +26,7 @@ class LessonStatistic(db.Model):
chars_per_minute = db.Column(db.Float, default=0.0)
error_rate = db.Column(db.Float, default=0.0)
wpm = db.Column(db.Float, default=0.0)
+ training_duration = db.Column(db.Float, default=0.0) # Trainingszeit in Sekunden
created_at = db.Column(db.DateTime, default=db.func.now(), index=True) # Index für zeitbasierte Queries
__table_args__ = (
diff --git a/static/js/main.js b/static/js/main.js
index 54b3a3c..12d4eee 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -113,6 +113,7 @@ class TypewriterApp {
this.sessionManager = new SessionManager(this.csrfToken);
this.sessionManager.initialize({
totalElapsedTime: window.totalElapsedTime || 0,
+ netTrainingTime: window.netTrainingTime || 0,
keyStrokeCount: window.keyStrokeCount || 0,
isPaused: window.isPaused || false,
currentLessonIndex: window.currentLessonIndex || 0
@@ -207,6 +208,7 @@ class TypewriterApp {
}
this.updateStatistics();
+ this.updateTrainingTimeDisplay();
});
}
@@ -294,12 +296,32 @@ class TypewriterApp {
}
}
+ /**
+ * Update training time display
+ */
+ updateTrainingTimeDisplay() {
+ const netTime = this.sessionManager.getNetTrainingTime();
+ const hours = Math.floor(netTime / 3600);
+ const minutes = Math.floor((netTime % 3600) / 60);
+ const seconds = Math.floor(netTime % 60);
+
+ const timeStr = hours > 0
+ ? `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
+ : `${minutes}:${String(seconds).padStart(2, '0')}`;
+
+ const timeElement = document.getElementById('training-time-value');
+ if (timeElement) {
+ timeElement.textContent = timeStr;
+ }
+ }
+
/**
* Update text display
* @param {boolean} isKeystroke - Whether this is triggered by a keystroke
*/
updateDisplay(isKeystroke = false) {
this.textDisplayManager.updateDisplay(this.userInput, this.currentCharIndex, isKeystroke);
+ this.updateTrainingTimeDisplay();
}
/**
@@ -368,6 +390,7 @@ class TypewriterApp {
// Get final statistics
const elapsedTime = this.sessionManager.getElapsedTime();
+ const trainingDuration = this.sessionManager.getNetTrainingTime();
const stats = this.statisticsManager.calculateStatistics(
this.userInput,
this.fullText,
@@ -382,6 +405,7 @@ class TypewriterApp {
stats.charsPerMinute,
stats.errorRate,
stats.wpm,
+ trainingDuration,
this.csrfToken
);
console.log('Lektion beendet! Statistiken gespeichert.');
diff --git a/static/js/metronome.js b/static/js/metronome.js
index 3607137..e44cdcd 100644
--- a/static/js/metronome.js
+++ b/static/js/metronome.js
@@ -45,8 +45,8 @@ export class AdaptiveMetronome {
this.speed *= 1.01; // In target range - gentle acceleration
}
- // Speed limits (configurable max_speed, absolute min 40, absolute max 200)
- this.speed = Math.max(40, Math.min(Math.min(200, this.max_speed), this.speed));
+ // Speed limits (configurable max_speed, absolute min 20, absolute max 200)
+ this.speed = Math.max(20, Math.min(Math.min(200, this.max_speed), this.speed));
return this.speed;
}
}
@@ -75,6 +75,12 @@ export class MetronomePlayer {
oscillator.type = 'sine';
oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + duration / 1000);
+
+ // Clean up after sound completes
+ oscillator.onended = () => {
+ oscillator.disconnect();
+ gainNode.disconnect();
+ };
}
createClickSound() {
diff --git a/static/js/script.js.old b/static/js/script.js.old
deleted file mode 100644
index 29fe0bc..0000000
--- a/static/js/script.js.old
+++ /dev/null
@@ -1,1121 +0,0 @@
-// Theme Management
-function initializeTheme() {
- // Beim ersten Start Light-Modus, sonst gespeicherten Modus verwenden
- const savedTheme = localStorage.getItem('theme') || 'light';
- document.body.setAttribute('data-bs-theme', savedTheme);
- updateThemeButton(savedTheme);
-}
-
-function toggleTheme() {
- const currentTheme = document.body.getAttribute('data-bs-theme');
- const newTheme = currentTheme === 'light' ? 'dark' : 'light';
- document.body.setAttribute('data-bs-theme', newTheme);
- localStorage.setItem('theme', newTheme);
- updateThemeButton(newTheme);
-}
-
-function updateThemeButton(theme) {
- const sunIcon = document.getElementById('theme-toggle-sun');
- const moonIcon = document.getElementById('theme-toggle-moon');
- if (theme === 'dark') {
- sunIcon.style.display = 'none';
- moonIcon.style.display = 'block';
- } else {
- sunIcon.style.display = 'block';
- moonIcon.style.display = 'none';
- }
-}
-
-document.addEventListener('DOMContentLoaded', function() {
- // Lucide Icons initialisieren
- lucide.createIcons();
-
- // CSRF-Token aus Meta-Tag holen
- const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
-
- // Theme initialisieren
- initializeTheme();
- document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
-
- // Hilfe Modal initialisieren
- document.getElementById('help-toggle').addEventListener('click', function() {
- const helpModal = new bootstrap.Modal(document.getElementById('helpModal'));
- helpModal.show();
- });
-
- // Settings Link initialisieren
- document.getElementById('settings-toggle').addEventListener('click', function() {
- window.location.href = '/settings';
- });
-
- // Aktuelle Textdaten - mit Fallback falls undefiniert
- let fullText = window.fullText || "Kein Text verfügbar. Bitte laden Sie die Seite neu.";
- let currentCharIndex = window.currentCharIndex || 0;
- let lastStartTime = null;
- let totalElapsedTime = window.totalElapsedTime || 0; // Aus der Session geladen
- let isPaused = window.isPaused || false; // Aus der Session geladen
- let timerInterval = null;
- let userInput = window.lastText || ''; // Gesamte Eingabe - aus der Datenbank geladen
- let keyStrokeCount = window.keyStrokeCount || 0; // Zähler für alle Tastaturanschläge (inkl. Backspace) - aus der Session geladen
- let lines = fullText.split('\n');
- let userScrolled = false; // Flag für manuelles Scrollen
- let firstVisibleLine = 0; // Index der obersten sichtbaren Zeile
-
- // HTML-Escape-Funktion für sichere DOM-Manipulation
- function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
-
- // Volume-Toggle initialisieren - Steuert jetzt das Metronom
- // Standardmäßig ist das Metronom an (volume-2 angezeigt)
- let metronomeEnabled = true;
- const volumeOn = document.getElementById('volume-on');
- const volumeOff = document.getElementById('volume-off');
- // Initiale Anzeige: volume-2 anzeigen, volume-x verstecken
- volumeOn.style.display = 'block';
- volumeOff.style.display = 'none';
-
- // Adaptive Metronom-Klasse
- class AdaptiveMetronome {
- constructor() {
- this.speed = 60; // Startgeschwindigkeit in BPM
- this.accuracy_history = []; // Letzte 50 Ergebnisse (1=korrekt, 0=falsch)
- this.consecutive_correct = 0; // Aufeinanderfolgende korrekte Zeichen
- this.target_accuracy = 0.96; // Zielgenauigkeit (96%)
- }
-
- process_keystroke(is_correct, current_difficulty = 1.0) {
- // 1. History aktualisieren
- this.accuracy_history.push(is_correct ? 1.0 : 0.0);
- if (this.accuracy_history.length > 50) {
- this.accuracy_history.shift();
- }
-
- // 2. Konsekutive korrekte Zeichen zählen
- if (is_correct) {
- this.consecutive_correct += 1;
- } else {
- this.consecutive_correct = 0;
- }
-
- // 3. Geschwindigkeit anpassen
- return this._adjust_speed(current_difficulty);
- }
-
- _adjust_speed(difficulty) {
- if (this.accuracy_history.length === 0) {
- return this.speed;
- }
-
- let current_accuracy = this.accuracy_history.reduce((a, b) => a + b, 0) / this.accuracy_history.length;
- let adjusted_target = this.target_accuracy - (difficulty * 0.05);
-
- // Mehrere Anpassungsstrategien kombinieren:
- if (this.consecutive_correct >= 15) {
- // Belohnung für längere Fehlerfreiheit
- this.speed *= 1.03;
- } else if (current_accuracy > adjusted_target + 0.04) {
- // Zu einfach - stärker beschleunigen
- this.speed *= 1.02;
- } else if (current_accuracy < adjusted_target - 0.06) {
- // Zu schwierig - stärker bremsen
- this.speed *= 0.92;
- } else if (current_accuracy < adjusted_target) {
- // Leicht unter Ziel - sanft bremsen
- this.speed *= 0.97;
- } else {
- // Im Zielbereich - sanft beschleunigen
- this.speed *= 1.01;
- }
-
- // Geschwindigkeitsgrenzen
- this.speed = Math.max(40, Math.min(200, this.speed));
- return this.speed;
- }
- }
-
- // Metronom State - Initialisierung
- let metronomeInterval = null;
- let audioContext = null;
- let metronomeSound = window.metronomeSound || 'beep';
- let metronomeMode = window.metronomeMode || 'automatic'; // automatic or explicit
- let targetErrorRate = window.targetErrorRate || 5; // Prozent
- let speedDisplay = window.speedDisplay || 'zpm'; // zpm or wpm
- let currentErrorRate = 0;
- let currentSpeed = 0; // Aktuelle Tippgeschwindigkeit in ZPM
-
- // Adaptive Metronom-Instanz
- let adaptiveMetronome = new AdaptiveMetronome();
-
- // Metronom-Geschwindigkeit basierend auf Modus initialisieren
- let metronomeBPM;
- if (metronomeMode === 'explicit') {
- metronomeBPM = window.metronomeSpeed || 60;
- } else {
- metronomeBPM = window.metronomeBPM || 60;
- }
-
- // Metronom-Funktionen
- function createBeepSound(vol, freq, duration) {
- if (!audioContext) {
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
- }
- const oscillator = audioContext.createOscillator();
- const gainNode = audioContext.createGain();
-
- oscillator.connect(gainNode);
- gainNode.connect(audioContext.destination);
-
- gainNode.gain.value = vol;
- oscillator.frequency.value = freq;
- oscillator.type = 'sine';
-
- oscillator.start();
- setTimeout(function() {
- oscillator.stop();
- }, duration);
- }
-
- function createClickSound() {
- if (!audioContext) {
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
- }
- const bufferSize = audioContext.sampleRate * 0.01;
- const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
- const data = buffer.getChannelData(0);
-
- for (let i = 0; i < bufferSize; i++) {
- data[i] = Math.random() * 2 - 1;
- }
-
- const source = audioContext.createBufferSource();
- source.buffer = buffer;
-
- const gainNode = audioContext.createGain();
- gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
- gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.1);
-
- source.connect(gainNode);
- gainNode.connect(audioContext.destination);
- source.start();
- }
-
- function playMetronomeSound() {
- // Wenn das Metronom ausgeschaltet ist, keinen Sound abspielen
- if (!metronomeEnabled) {
- // Aber visuelles Feedback trotzdem anzeigen
- const cursorElement = document.querySelector('.cursor-char');
- if (cursorElement) {
- cursorElement.style.animation = 'pulse 0.3s';
- setTimeout(() => {
- cursorElement.style.animation = '';
- }, 300);
- }
- return;
- }
-
- if (metronomeSound === 'beep') {
- createBeepSound(0.1, 800, 50);
- } else {
- createClickSound();
- }
-
- // Visuelles Feedback: Cursor pulsieren lassen
- const cursorElement = document.querySelector('.cursor-char');
- if (cursorElement) {
- cursorElement.style.animation = 'pulse 0.3s';
- setTimeout(() => {
- cursorElement.style.animation = '';
- }, 300);
- }
- }
-
- function startMetronome() {
- stopMetronome();
- const interval = (60 / metronomeBPM) * 1000;
- playMetronomeSound(); // Sofort abspielen
- metronomeInterval = setInterval(playMetronomeSound, interval);
- }
-
- function stopMetronome() {
- if (metronomeInterval) {
- clearInterval(metronomeInterval);
- metronomeInterval = null;
- }
- }
-
-
- // Starte Metronom standardmäßig
- if (!isPaused) {
- startMetronome();
- }
-
- document.getElementById('volume-toggle').addEventListener('click', function() {
- metronomeEnabled = !metronomeEnabled;
- if (metronomeEnabled) {
- volumeOn.style.display = 'block';
- volumeOff.style.display = 'none';
- if (!isPaused) {
- startMetronome();
- }
- } else {
- volumeOn.style.display = 'none';
- volumeOff.style.display = 'block';
- stopMetronome();
- }
- // Einstellungen an Server senden
- fetch('/set_metronome', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': csrfToken
- },
- body: JSON.stringify({
- enabled: metronomeEnabled,
- bpm: metronomeBPM,
- sound: metronomeSound,
- mode: metronomeMode,
- speed: metronomeBPM, // Verwende die aktuelle BPM
- target_error_rate: targetErrorRate
- })
- });
- });
-
- // ErrorLock State - Standardmäßig geöffnet (errorLock = false)
- let errorLock = false;
- const lockClosed = document.getElementById('lock-closed');
- const lockOpen = document.getElementById('lock-open');
- // Initiale Anzeige: offenes Schloss anzeigen, geschlossenes verstecken
- lockClosed.style.display = 'none';
- lockOpen.style.display = 'block';
-
-
- // Lock-Toggle initialisieren
- document.getElementById('lock-toggle').addEventListener('click', function() {
- if (errorLock) {
- // Wechsel zu unlocked
- errorLock = false;
- lockClosed.style.display = 'none';
- lockOpen.style.display = 'block';
- } else {
- // Wechsel zu locked
- errorLock = true;
- lockClosed.style.display = 'block';
- lockOpen.style.display = 'none';
- }
- });
-
- // Play/Pause-Toggle initialisieren
- const playPauseToggle = document.getElementById('play-pause-toggle');
- if (playPauseToggle) {
- playPauseToggle.addEventListener('click', function() {
- if (isPaused) {
- // Fortsetzen
- isPaused = false;
- lastStartTime = new Date();
- document.getElementById('play-pause-pause').style.display = 'block';
- document.getElementById('play-pause-play').style.display = 'none';
- if (metronomeEnabled) {
- startMetronome();
- }
- } else {
- // Pausieren
- isPaused = true;
- if (lastStartTime !== null) {
- totalElapsedTime += (new Date() - lastStartTime);
- lastStartTime = null;
- }
- document.getElementById('play-pause-pause').style.display = 'none';
- document.getElementById('play-pause-play').style.display = 'block';
- stopMetronome();
- }
- updateStatistics();
- });
- }
-
- const targetTextElement = document.getElementById('target-text');
- if (!targetTextElement) {
- console.error('Target text element not found');
- }
- const nextBtn = document.getElementById('next-btn');
- const endBtn = document.getElementById('end-btn');
-
- // Statistik-Elemente
- const speedElement = document.getElementById('speed');
- const errorRateElement = document.getElementById('error-rate');
- const correctElement = document.getElementById('correct');
- const incorrectElement = document.getElementById('incorrect');
- const totalElement = document.getElementById('total');
- const wpmElement = document.getElementById('wpm');
-
- // Setze Fokus auf das Dokument, um Tastatureingaben zu erfassen
- document.addEventListener('click', function() {
- document.body.focus();
- });
- document.body.tabIndex = -1;
- document.body.focus();
-
- // Event-Listener für manuelles Scrollen
- targetTextElement.addEventListener('scroll', function() {
- userScrolled = true;
- // Aktualisiere firstVisibleLine basierend auf der Scroll-Position
- const lineHeight = targetTextElement.scrollHeight / lines.length;
- firstVisibleLine = Math.floor(targetTextElement.scrollTop / lineHeight);
- });
-
- // Berechne die aktuelle Zeile basierend auf currentCharIndex
- function getCurrentLineIndex() {
- let charCount = 0;
- for (let i = 0; i < lines.length; i++) {
- const lineLength = lines[i].length + (i < lines.length - 1 ? 1 : 0); // +1 für Zeilenumbruch außer bei der letzten Zeile
- if (currentCharIndex < charCount + lineLength) {
- return i;
- }
- charCount += lineLength;
- }
- return lines.length - 1;
- }
-
- // Funktion zur Berechnung der Länge des korrekten Präfixes
- function getCorrectPrefixLength() {
- let correct = 0;
- for (let i = 0; i < userInput.length; i++) {
- if (i < fullText.length && userInput[i] === fullText[i]) {
- correct++;
- } else {
- break;
- }
- }
- return correct;
- }
-
- // Zeige den Text an
- updateTextDisplay();
-
- // Verhindere Copy & Paste für das Vorlagenfeld
- targetTextElement.addEventListener('copy', function(event) {
- event.preventDefault();
- alert('Kopieren aus der Vorlage ist nicht erlaubt! Bitte tippen Sie den Text manuell.');
- });
-
- targetTextElement.addEventListener('cut', function(event) {
- event.preventDefault();
- alert('Ausschneiden aus der Vorlage ist nicht erlaubt!');
- });
-
- targetTextElement.addEventListener('contextmenu', function(event) {
- event.preventDefault();
- return false;
- });
-
- // Tastatureingaben direkt erfassen
- document.addEventListener('keydown', function(event) {
- // Verhindere Standardaktionen für Steuertasten
- if (event.ctrlKey || event.metaKey) {
- event.preventDefault();
- return;
- }
-
- // Im Pause-Modus sind keine Tasten erlaubt
- if (isPaused) {
- event.preventDefault();
- return;
- }
-
- // Behandle Pfeiltasten
- if (event.key === 'ArrowLeft') {
- event.preventDefault();
- if (currentCharIndex > 0) {
- currentCharIndex--;
- updateTextDisplay();
- }
- return;
- }
-
- if (event.key === 'ArrowRight') {
- event.preventDefault();
- if (currentCharIndex < fullText.length) {
- currentCharIndex++;
- updateTextDisplay();
- }
- return;
- }
-
- // Behandle Backspace
- if (event.key === 'Backspace') {
- event.preventDefault();
- if (currentCharIndex > 0) {
- userInput = userInput.slice(0, currentCharIndex - 1) + userInput.slice(currentCharIndex);
- currentCharIndex--;
- keyStrokeCount++;
- if (!lastStartTime) {
- lastStartTime = new Date();
- }
- updateTextDisplay();
- updateStatistics();
- }
- return;
- }
-
- // Behandle Enter
- if (event.key === 'Enter') {
- event.preventDefault();
- if (currentCharIndex < fullText.length && fullText[currentCharIndex] === '\n') {
- processKeystroke('\n', true);
- }
- return;
- }
-
- // Ignoriere andere Steuertasten
- if (event.key.length > 1 && event.key !== ' ') {
- return;
- }
-
- // ErrorLock prüfen
- if (errorLock) {
- const correctPrefixLength = getCorrectPrefixLength();
- if (currentCharIndex !== correctPrefixLength) {
- event.preventDefault();
- return;
- }
- }
-
- // Normale Zeichen
- event.preventDefault();
- if (currentCharIndex < fullText.length) {
- processKeystroke(event.key, event.key === fullText[currentCharIndex]);
- }
- });
-
- function processKeystroke(key, isCorrect) {
- // Füge Zeichen hinzu oder ersetze es
- if (currentCharIndex < userInput.length) {
- userInput = userInput.slice(0, currentCharIndex) + key + userInput.slice(currentCharIndex + 1);
- } else {
- userInput = userInput.slice(0, currentCharIndex) + key + userInput.slice(currentCharIndex);
- }
- currentCharIndex++;
- keyStrokeCount++;
-
- if (!lastStartTime) {
- lastStartTime = new Date();
- }
-
- // Metronom anpassen
- if (metronomeMode === 'automatic') {
- metronomeBPM = adaptiveMetronome.process_keystroke(isCorrect, 1.0);
- if (metronomeEnabled && !isPaused) {
- startMetronome();
- }
- }
-
- updateTextDisplay();
- updateStatistics();
-
- // Lektion abschließen
- if (currentCharIndex === fullText.length) {
- setTimeout(saveAndResetLesson, 100);
- }
- }
-
- function saveAndResetLesson() {
- const wasPlaying = !isPaused;
- if (wasPlaying) {
- isPaused = true;
- if (lastStartTime) {
- totalElapsedTime += (new Date() - lastStartTime);
- lastStartTime = null;
- }
- stopMetronome();
- }
-
- const lessonIndex = window.currentLessonIndex;
- const speedElement = document.getElementById('speed');
- const errorRateElement = document.getElementById('error-rate');
- const wpmElement = document.getElementById('wpm');
-
- const charsPerMinute = speedElement ? parseInt(speedElement.textContent) : 0;
- const errorRate = errorRateElement ? parseFloat(errorRateElement.textContent) : 0;
- const wpm = wpmElement ? parseInt(wpmElement.textContent) : 0;
-
- fetch('/save_lesson_statistics', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': csrfToken
- },
- 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') {
- console.log('Lektion beendet! Statistiken gespeichert.');
- }
- })
- .catch(error => {
- console.error('Fehler beim Speichern:', error);
- })
- .finally(() => {
- currentCharIndex = 0;
- userInput = '';
- userScrolled = false;
- targetTextElement.scrollTop = 0;
- totalElapsedTime = 0;
- lastStartTime = null;
- keyStrokeCount = 0;
-
- if (wasPlaying) {
- isPaused = false;
- lastStartTime = new Date();
- const playPausePause = document.getElementById('play-pause-pause');
- const playPausePlay = document.getElementById('play-pause-play');
- if (playPausePause && playPausePlay) {
- playPausePause.style.display = 'block';
- playPausePlay.style.display = 'none';
- }
- }
-
- updateTextDisplay();
- updateStatistics();
-
- if (metronomeEnabled && !isPaused) {
- startMetronome();
- }
- });
- }
-
-
- // Funktion zum Aktualisieren der Lektionsmarkierung
- function updateLessonHighlight(lessonIndex) {
- document.querySelectorAll('.lesson-link').forEach(link => {
- const index = parseInt(link.getAttribute('data-lesson-index'));
- if (index === lessonIndex) {
- link.classList.add('fw-bold', 'text-danger');
- link.classList.remove('text-primary');
- } else {
- link.classList.remove('fw-bold', 'text-danger');
- link.classList.add('text-primary');
- }
- });
- }
-
- // Event-Listener für Lektions-Links
- document.querySelectorAll('.lesson-link').forEach(link => {
- link.addEventListener('click', function() {
- const lessonIndex = parseInt(this.getAttribute('data-lesson-index'));
- // Stoppe Timer und setze Zeit zurück
- if (timerInterval) {
- clearInterval(timerInterval);
- timerInterval = null;
- }
- lastStartTime = null;
-
- fetch('/set_lesson', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': csrfToken
- },
- body: JSON.stringify({
- lesson_index: lessonIndex
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.error) {
- alert('Fehler: ' + data.error);
- } else {
- fullText = data.text;
- currentCharIndex = 0;
- userInput = '';
- lastStartTime = null;
- totalElapsedTime = 0;
- isPaused = false;
- keyStrokeCount = 0; // Zurücksetzen des Tastaturanschlagszählers
- lines = fullText.split('\n');
- userScrolled = false;
- firstVisibleLine = 0;
- targetTextElement.scrollTop = 0;
- updateTextDisplay();
- document.getElementById('lesson-title').textContent = data.title;
- // Setze Play/Pause-Toggle zurück auf Pause
- document.getElementById('play-pause-pause').style.display = 'block';
- document.getElementById('play-pause-play').style.display = 'none';
- updateStatistics();
- // Aktualisiere die Markierung der aktuellen Lektion
- updateLessonHighlight(lessonIndex);
- }
- })
- .catch(error => console.error('Fehler:', error));
- });
- });
-
- // Nächster Text Button
- if (nextBtn) {
- nextBtn.addEventListener('click', function() {
- console.log('Next button clicked');
- // Stoppe Timer und setze Zeit zurück
- if (timerInterval) {
- clearInterval(timerInterval);
- timerInterval = null;
- }
-
- fetch('/next_text', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': csrfToken
- }
- })
- .then(response => response.json())
- .then(data => {
- if (data.error) {
- alert('Fehler: ' + data.error);
- return;
- }
- fullText = data.text;
- currentCharIndex = 0;
- userInput = '';
- lastStartTime = null;
- totalElapsedTime = 0;
- isPaused = false;
- keyStrokeCount = 0; // Zurücksetzen des Tastaturanschlagszählers
- lines = fullText.split('\n');
- userScrolled = false;
- firstVisibleLine = 0;
- targetTextElement.scrollTop = 0;
- updateTextDisplay();
- // Aktualisiere den Lektionstitel, falls das Element existiert
- const lessonTitleElement = document.getElementById('lesson-title');
- if (lessonTitleElement) {
- lessonTitleElement.textContent = data.title;
- }
- // Setze Play/Pause-Toggle zurück auf Pause
- const playPausePause = document.getElementById('play-pause-pause');
- const playPausePlay = document.getElementById('play-pause-play');
- if (playPausePause && playPausePlay) {
- playPausePause.style.display = 'block';
- playPausePlay.style.display = 'none';
- }
- updateStatistics();
- // Aktualisiere die Markierung der aktuellen Lektion
- updateLessonHighlight(data.index);
- })
- .catch(error => {
- console.error('Fehler:', error);
- alert('Fehler beim Laden des nächsten Textes: ' + error.message);
- });
- });
- } else {
- console.error('Next button not found');
- }
-
- // Beenden Button
- if (endBtn) {
- endBtn.addEventListener('click', function() {
- console.log('End button clicked');
- // Hier könnten wir die aktuelle Statistik speichern
- fetch('/end_session', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': csrfToken
- }
- })
- .then(response => response.json())
- .then(data => {
- alert('Sitzung beendet. Fortschritt gespeichert.');
- // window.close(); // Funktioniert nicht in allen Browsern
- // Stattdessen: Zur Startseite navigieren oder Seite neu laden
- window.location.href = '/';
- })
- .catch(error => {
- console.error('Fehler:', error);
- alert('Fehler beim Beenden der Sitzung: ' + error.message);
- });
- });
- } else {
- console.error('End button not found');
- }
-
- // Statistik aktualisieren
- function updateStatistics() {
- // Berechne verstrichene Zeit
- let elapsedTime = totalElapsedTime;
- if (lastStartTime !== null) {
- elapsedTime += (new Date() - lastStartTime);
- }
- elapsedTime = elapsedTime / 1000; // in Sekunden
-
- // 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 < userInput.length; i++) {
- if (i < fullText.length && userInput[i] === fullText[i]) {
- correctKeyStrokes++;
- } else {
- break;
- }
- }
-
- let incorrectKeyStrokes = totalKeyStrokes - correctKeyStrokes;
- let charsPerMinute = elapsedTime > 0 ? (correctKeyStrokes / elapsedTime) * 60 : 0;
- let errorRate = totalKeyStrokes > 0 ? (incorrectKeyStrokes * 100.0) / totalKeyStrokes : 0;
- let wpm = elapsedTime > 0 ? (correctKeyStrokes / 5) / (elapsedTime / 60) : 0;
-
- // Hole alle Statistik-Elemente
- const speedElement = document.getElementById('speed');
- const errorRateElement = document.getElementById('error-rate');
- const correctElement = document.getElementById('correct');
- const incorrectElement = document.getElementById('incorrect');
- const totalElement = document.getElementById('total');
- const wpmElement = document.getElementById('wpm');
- const speedLabel = document.querySelector('#speed-label');
-
- // Aktualisiere die Anzeige basierend auf der gewählten Geschwindigkeitsanzeige, falls Elemente existieren
- if (speedDisplay === 'wpm') {
- if (speedElement) speedElement.textContent = Math.round(wpm);
- if (speedLabel) speedLabel.textContent = 'WPM:';
- } else {
- if (speedElement) speedElement.textContent = Math.round(charsPerMinute);
- if (speedLabel) speedLabel.textContent = 'ZPM:';
- }
- if (errorRateElement) errorRateElement.textContent = errorRate.toFixed(1);
- if (correctElement) correctElement.textContent = correctKeyStrokes;
- if (incorrectElement) incorrectElement.textContent = incorrectKeyStrokes;
- if (totalElement) totalElement.textContent = totalKeyStrokes;
- if (wpmElement) wpmElement.textContent = Math.round(wpm);
-
- // Speichere aktuelle Werte für Metronom-Anpassung
- currentSpeed = Math.round(charsPerMinute);
- currentErrorRate = errorRate;
-
- // Aktualisiere die Textfarben
- updateTextDisplay();
- }
-
- // Hilfsfunktion zum Überprüfen, ob eine Zeile komplett korrekt eingegeben wurde
- function isLineComplete(lineIndex) {
- if (lineIndex >= lines.length) return false;
-
- // Berechne Start- und Endindex der Zeile im Gesamttext
- let startIndex = 0;
- for (let i = 0; i < lineIndex; i++) {
- startIndex += lines[i].length + 1; // +1 für Zeilenumbruch
- }
- const endIndex = startIndex + lines[lineIndex].length;
-
- // Prüfe, ob alle Zeichen der Zeile korrekt eingegeben wurden
- for (let i = startIndex; i < endIndex; i++) {
- if (i >= userInput.length || userInput[i] !== fullText[i]) {
- return false;
- }
- }
-
- // Für Zeilen, die nicht die letzte sind: Prüfe auch den Zeilenumbruch
- if (lineIndex < lines.length - 1) {
- const newlineIndex = endIndex;
- if (newlineIndex >= userInput.length || userInput[newlineIndex] !== '\n') {
- return false;
- }
- }
-
- return true;
- }
-
- // Funktion zum automatischen Scrollen
- function checkAutoScroll() {
- // Prüfe, ob die oberste sichtbare Zeile komplett korrekt eingegeben wurde
- if (isLineComplete(firstVisibleLine)) {
- // Scrolle um eine Zeile nach unten
- const lineHeight = targetTextElement.scrollHeight / lines.length;
- firstVisibleLine++;
- targetTextElement.scrollTop = firstVisibleLine * lineHeight;
- }
- }
-
- // Hilfsfunktion zum Hervorheben eines Zeichens
- function highlightChar(char, type, isCursor = false, incorrectChar = null) {
- let className = `highlight-${type}`;
- if (isCursor) {
- className += ' cursor-char';
- }
- // Escape HTML-Zeichen für Sicherheit
- const escapedChar = escapeHtml(char);
- if (char === ' ') {
- return `·`;
- } else if (char === '\t') {
- return `→`;
- } else if (char === '\n') {
- return `↵
`;
- } else {
- if (type === 'incorrect' && incorrectChar !== null) {
- const escapedIncorrectChar = escapeHtml(incorrectChar);
- return `${escapedChar}${escapedIncorrectChar}`;
- } else {
- return `${escapedChar}`;
- }
- }
- }
-
- // Cache für die letzte gerenderte Version
- let lastRenderedUserInputLength = -1;
- let lastCursorPosition = -1;
- let lastCurrentLineIndex = -1;
-
- // Text hervorheben mit natürlichen Zeilenumbrüchen
- function updateTextDisplay() {
- // Vereinfachte Version: Zeige den gesamten Text mit Zeilenumbrüchen an
- let newHTML = '';
- let charCount = 0;
-
- for (let j = 0; j < lines.length; j++) {
- const line = lines[j];
- let highlightedLine = '';
-
- for (let i = 0; i < line.length; i++) {
- const currentChar = line[i];
- const isCursor = (charCount === currentCharIndex);
-
- if (charCount < userInput.length) {
- if (userInput[charCount] === currentChar) {
- highlightedLine += highlightChar(currentChar, 'correct', isCursor);
- } else {
- highlightedLine += highlightChar(currentChar, 'incorrect', isCursor, userInput[charCount]);
- }
- } else {
- highlightedLine += highlightChar(currentChar, 'neutral', isCursor);
- }
- charCount++;
- }
-
- // Zeilenumbruch nach jeder Zeile (außer der letzten)
- if (j < lines.length - 1) {
- const isCursor = (charCount === currentCharIndex);
- if (charCount < userInput.length) {
- if (userInput[charCount] === '\n') {
- highlightedLine += highlightChar('\n', 'correct', isCursor);
- } else {
- highlightedLine += highlightChar('\n', 'incorrect', isCursor, userInput[charCount]);
- }
- } else {
- highlightedLine += highlightChar('\n', 'neutral', isCursor);
- }
- charCount++;
- }
-
- // Füge die Zeile als div hinzu
- const currentLineIndex = getCurrentLineIndex();
- newHTML += `${highlightedLine}
`;
- }
-
- targetTextElement.innerHTML = newHTML;
-
- // Automatisches Scrollen: aktuelle Zeile soll oben angezeigt werden
- if (!userScrolled) {
- // Verwende setTimeout, um sicherzustellen, dass das DOM vollständig gerendert ist
- setTimeout(() => {
- const currentLineIndex = getCurrentLineIndex();
- const lineElements = targetTextElement.getElementsByClassName('text-line');
-
- if (lineElements.length > 0) {
- // Verwende die tatsächliche Höhe der ersten Zeile
- const lineHeight = lineElements[0].offsetHeight;
- const containerHeight = targetTextElement.clientHeight;
-
- // Falls Zeilenhöhe 0 ist, versuche es erneut nach 10ms
- if (lineHeight === 0) {
- setTimeout(updateTextDisplay, 10);
- return;
- }
-
- // Berechne die gewünschte Scroll-Position: aktuelle Zeile soll oben erscheinen
- let desiredScrollTop = currentLineIndex * lineHeight;
-
- // Begrenze die Scroll-Position auf den maximal möglichen Wert
- const maxScrollTop = targetTextElement.scrollHeight - containerHeight;
- desiredScrollTop = Math.min(desiredScrollTop, maxScrollTop);
- desiredScrollTop = Math.max(0, desiredScrollTop);
-
- // Setze die Scroll-Position
- targetTextElement.scrollTop = desiredScrollTop;
- }
- }, 0);
- }
- }
-
- // Einfache Cursor-Aktualisierung (wird jetzt in updateTextDisplay integriert)
- // Diese Funktion ist nicht mehr notwendig, da der Cursor in updateTextDisplay gesetzt wird
-
- // 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)
- document.querySelectorAll('.statistics-link').forEach(link => {
- link.addEventListener('click', function() {
- console.log('Statistics link clicked');
- // Speichere Fortschritt synchron vor dem Navigieren
- saveProgress();
- // Navigiere zur Statistik-Seite
- window.location.href = '/statistics';
- });
- });
-
-
- // Toggle-Funktionalität für Fortschritt und Statistik (beide gleichzeitig)
- function initializeProgressStatsToggle(retryCount = 0) {
- const performanceBtn = document.getElementById('performance-btn');
- const progressContainer = document.querySelector('.progress-stats-container');
-
- if (!performanceBtn || !progressContainer) {
- if (retryCount < 3) {
- console.warn('Performance button or progress container not found, retrying...');
- setTimeout(() => initializeProgressStatsToggle(retryCount + 1), 500);
- } else {
- console.warn('Performance button or progress container not found after retries');
- }
- return;
- }
-
- // Beim ersten Start abgeschaltet, sonst gespeicherten Zustand verwenden
- const progressStatsVisible = localStorage.getItem('progressStatsVisible') === 'true';
-
- // Setze initialen Zustand
- setProgressStatsVisibility(progressStatsVisible);
-
- // Event-Listener für Performance-Button
- performanceBtn.addEventListener('click', function() {
- const isVisible = progressContainer.classList.contains('visible');
- setProgressStatsVisibility(!isVisible);
- localStorage.setItem('progressStatsVisible', !isVisible);
- });
- }
-
- function setProgressStatsVisibility(visible) {
- const progressContainer = document.querySelector('.progress-stats-container');
- const performanceBtn = document.getElementById('performance-btn');
-
- if (!progressContainer || !performanceBtn) {
- return;
- }
-
- if (visible) {
- progressContainer.classList.add('visible');
- performanceBtn.classList.remove('btn-light-custom');
- performanceBtn.classList.add('btn-primary');
- } else {
- progressContainer.classList.remove('visible');
- performanceBtn.classList.remove('btn-primary');
- performanceBtn.classList.add('btn-light-custom');
- }
- }
-
-
- // Speichere Fortschritt beim Verlassen der Seite
- window.addEventListener('beforeunload', function() {
- saveProgress();
- });
-
- // Funktion zum Speichern des Fortschritts
- function saveProgress() {
- const data = {
- user_input: userInput,
- cursor_position: currentCharIndex,
- current_text: fullText,
- elapsed_time: totalElapsedTime / 1000,
- total_elapsed_time: totalElapsedTime,
- key_stroke_count: keyStrokeCount,
- is_paused: isPaused
- };
-
- // Verwende fetch mit keepalive für zuverlässiges Speichern beim Seitenverlassen
- // keepalive stellt sicher, dass der Request auch nach dem Schließen der Seite abgeschlossen wird
- fetch('/update_progress', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': csrfToken
- },
- body: JSON.stringify(data),
- keepalive: true // Wichtig: Request bleibt aktiv auch nach Seitenverlassen
- }).catch(error => {
- console.error('Fehler beim Speichern des Fortschritts:', error);
- });
- }
-
- // Setze den Play/Pause-Button basierend auf dem geladenen Zustand
- if (isPaused) {
- document.getElementById('play-pause-pause').style.display = 'none';
- document.getElementById('play-pause-play').style.display = 'block';
- } else {
- document.getElementById('play-pause-pause').style.display = 'block';
- document.getElementById('play-pause-play').style.display = 'none';
- }
-
- // Initialisiere Statistik und Textfarben mit Verzögerung für zuverlässige Darstellung
- const initializeDisplay = () => {
- updateTextDisplay();
- updateStatistics();
-
- // Zweiter Versuch nach kurzer Verzögerung für fallback
- setTimeout(() => {
- updateTextDisplay();
- updateStatistics();
- }, 100);
- };
-
- // Warte auf nächstes Animation Frame für zuverlässige Darstellung
- requestAnimationFrame(initializeDisplay);
-
- // Initialisiere Toggle-Funktionalität
- initializeProgressStatsToggle();
-
- // Setze Fokus auf den Body für Tastatureingaben
- document.body.focus();
-
- // Debug-Informationen
- console.log('Script initialized');
- console.log('Full text length:', fullText.length);
- console.log('Current char index:', currentCharIndex);
- console.log('User input length:', userInput.length);
-});
-
-// Fehlerbehandlung für globale Ereignisse
-window.addEventListener('error', function(e) {
- console.error('JavaScript Fehler:', e.error);
-});
-
-// Sicherstellen, dass der Body fokussiert ist
-document.addEventListener('click', function() {
- if (document.activeElement !== document.body) {
- document.body.focus();
- }
-});
diff --git a/static/js/session.js b/static/js/session.js
index 1aa7950..b5981a7 100644
--- a/static/js/session.js
+++ b/static/js/session.js
@@ -8,6 +8,7 @@ export class SessionManager {
this.csrfToken = csrfToken;
this.lastStartTime = null;
this.totalElapsedTime = 0;
+ this.netTrainingTime = 0;
this.keyStrokeCount = 0;
this.isPaused = false;
this.currentLessonIndex = 0;
@@ -19,6 +20,7 @@ export class SessionManager {
*/
initialize(initialState) {
this.totalElapsedTime = initialState.totalElapsedTime || 0;
+ this.netTrainingTime = initialState.netTrainingTime || 0;
this.keyStrokeCount = initialState.keyStrokeCount || 0;
this.isPaused = initialState.isPaused || false;
this.currentLessonIndex = initialState.currentLessonIndex || 0;
@@ -40,6 +42,18 @@ export class SessionManager {
return elapsed / 1000; // Convert to seconds
}
+ /**
+ * Get net training time in seconds (without pauses)
+ * @returns {number} Net training time in seconds
+ */
+ getNetTrainingTime() {
+ let netTime = this.netTrainingTime;
+ if (!this.isPaused && this.lastStartTime !== null) {
+ netTime += (new Date() - this.lastStartTime);
+ }
+ return netTime / 1000;
+ }
+
/**
* Start or resume timing
*/
@@ -55,6 +69,7 @@ export class SessionManager {
pause() {
if (this.lastStartTime !== null) {
this.totalElapsedTime += (new Date() - this.lastStartTime);
+ this.netTrainingTime += (new Date() - this.lastStartTime);
this.lastStartTime = null;
}
this.isPaused = true;
@@ -97,6 +112,7 @@ export class SessionManager {
reset() {
this.lastStartTime = null;
this.totalElapsedTime = 0;
+ this.netTrainingTime = 0;
this.keyStrokeCount = 0;
this.isPaused = false;
}
@@ -116,6 +132,7 @@ export class SessionManager {
current_text: currentText,
elapsed_time: this.totalElapsedTime / 1000,
total_elapsed_time: this.totalElapsedTime,
+ net_training_time: this.netTrainingTime,
key_stroke_count: this.keyStrokeCount,
is_paused: this.isPaused
};
diff --git a/static/js/statistics.js b/static/js/statistics.js
index e5fb8df..d22e62c 100644
--- a/static/js/statistics.js
+++ b/static/js/statistics.js
@@ -86,10 +86,11 @@ export class StatisticsManager {
* @param {number} charsPerMinute - Characters per minute
* @param {number} errorRate - Error rate percentage
* @param {number} wpm - Words per minute
+ * @param {number} trainingDuration - Training duration in seconds
* @param {string} csrfToken - CSRF token
* @returns {Promise} Fetch promise
*/
- async saveLessonStatistics(lessonIndex, charsPerMinute, errorRate, wpm, csrfToken) {
+ async saveLessonStatistics(lessonIndex, charsPerMinute, errorRate, wpm, trainingDuration, csrfToken) {
try {
const response = await fetch('/save_lesson_statistics', {
method: 'POST',
@@ -101,7 +102,8 @@ export class StatisticsManager {
lesson_index: lessonIndex,
chars_per_minute: charsPerMinute,
error_rate: errorRate,
- wpm: wpm
+ wpm: wpm,
+ training_duration: trainingDuration
})
});
diff --git a/templates/hilfe.html b/templates/hilfe.html
index 96932e5..decfde0 100644
--- a/templates/hilfe.html
+++ b/templates/hilfe.html
@@ -7,12 +7,8 @@
+
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
- {{ hilfe_html|safe }}
+
+
+
+ {{ hilfe_html|safe }}
+
+
+
+
Deutsches Tastatur-Layout
+
 }})
+
Standard-Deutsches Tastaturlayout (QWERTZ)
+
+
+
-
+
← Zurück zum Trainer
-
-
-
+
+
+
+
+
-
-
-
📊 Deine Statistiken
-
-
-
{{ total_minutes|int }}
-
Minuten insgesamt
+
+
+
📊 Deine Statistiken
+
+
+
{{ total_minutes|int }}
+
Minuten insgesamt
+
+
+
{{ days_practiced }}
+
Tage geübt
+
+
+
{{ (total_minutes / days_practiced)|round(1) if days_practiced > 0 else 0 }}
+
Ø Minuten/Tag
+
+
-
-
{{ days_practiced }}
-
Tage geübt
+
+
+
+
Übungszeit der letzten 30 Tage
+
-
-
{{ (total_minutes / days_practiced)|round(1) if days_practiced > 0 else 0 }}
-
Ø Minuten/Tag
+
+
+
-
-
-
-
Übungszeit der letzten 30 Tage
-
-
-
-
-
@@ -226,8 +225,10 @@
}
});
- // Zeige Abschiedsnachricht
- document.body.innerHTML = '
👋 Auf Wiedersehen!
Du kannst dieses Fenster jetzt schließen.
';
+ // Zeige Abschiedsnachricht mit Theme-Unterstützung
+ const bgColor = savedTheme === 'dark' ? '#1a1a1a' : '#f8f9fa';
+ const textColor = savedTheme === 'dark' ? '#e0e0e0' : '#212529';
+ document.body.innerHTML = '
👋 Auf Wiedersehen!
Du kannst dieses Fenster jetzt schließen.
';
} catch (error) {
alert('Fehler beim Beenden: ' + error.message);
}
diff --git a/templates/statistics.html b/templates/statistics.html
index 617dd18..90f3ac4 100644
--- a/templates/statistics.html
+++ b/templates/statistics.html
@@ -73,6 +73,11 @@
+
+
+
+
+
@@ -158,12 +163,22 @@
function updateChart(data) {
const ctx = document.getElementById('statisticsChart').getContext('2d');
-
+
// Zerstöre vorheriges Diagramm falls vorhanden
if (currentChart) {
currentChart.destroy();
}
+ // Konvertiere Trainingszeit von Sekunden in Minuten für Anzeige
+ const trainingMinutes = data.training_duration.map(sec => sec / 60);
+
+ // Berechne Gesamttrainingszeit
+ const totalSeconds = data.training_duration.reduce((sum, sec) => sum + sec, 0);
+ const totalHours = Math.floor(totalSeconds / 3600);
+ const totalMinutes = Math.floor((totalSeconds % 3600) / 60);
+ document.getElementById('total-training-time').textContent =
+ `Gesamte Trainingszeit: ${totalHours}:${String(totalMinutes).padStart(2, '0')}`;
+
currentChart = new Chart(ctx, {
type: 'line',
data: {
@@ -195,6 +210,14 @@
backgroundColor: 'rgba(255, 0, 0, 0.1)',
tension: 0.1,
yAxisID: 'y1'
+ },
+ {
+ label: 'Zeit (Minuten)',
+ data: trainingMinutes,
+ borderColor: 'green',
+ backgroundColor: 'rgba(0, 255, 0, 0.1)',
+ tension: 0.1,
+ yAxisID: 'y2'
}
]
},
@@ -240,6 +263,20 @@
drawOnChartArea: false,
},
},
+ y2: {
+ type: 'linear',
+ display: true,
+ position: 'right',
+ title: {
+ display: true,
+ text: 'Zeit (Min)'
+ },
+ min: 0,
+ suggestedMax: Math.max(...trainingMinutes) * 1.1 || 1,
+ grid: {
+ drawOnChartArea: false,
+ },
+ },
}
}
});
diff --git a/templates/welcome.html b/templates/welcome.html
index 07a175b..34f39fa 100644
--- a/templates/welcome.html
+++ b/templates/welcome.html
@@ -9,138 +9,123 @@
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- 🎹
-
+
+
+ 🎹
+
-
-
Typewriter Trainer
-
{{ APP_VERSION }}
+
+
Typewriter Trainer
+
{{ APP_VERSION }}
-
-
- Lernen Sie das Zehnfingersystem
- mit adaptivem Metronom
-
+
+
+ Lernen Sie das Zehnfingersystem
+ mit adaptivem Metronom
+
-
-
-
- - ✓ 43 aufbauende Lektionen
- - ✓ Echtzeit-Statistiken & Präzisions-Anzeige
- - ✓ Adaptives Metronom mit max. Geschwindigkeit
- - ✓ Dark/Light Mode & persistente Einstellungen
-
-
+
+
+
+ - ✓ 43 aufbauende Lektionen
+ - ✓ Echtzeit-Statistiken & Präzisions-Anzeige
+ - ✓ Adaptives Metronom mit max. Geschwindigkeit
+ - ✓ Dark/Light Mode & persistente Einstellungen
+
+
-
-
+
+
-
-
-
-
-
-
- Entwickelt mit ❤️ für effektives Tipptraining
-
-
- GitHub
-
-
-