diff --git a/HILFE.md b/HILFE.md
index 947180a..e8f6002 100644
--- a/HILFE.md
+++ b/HILFE.md
@@ -77,6 +77,7 @@ Typewriter Trainer ist ein webbasierter Tipptrainer, der Ihnen hilft:
| **←** (Links) | Cursor nach links bewegen |
| **→** (Rechts) | Cursor nach rechts bewegen |
| **Enter** | Zeilenumbruch (wenn im Text vorhanden) |
+| **Strg+P** | Pausieren/Fortsetzen (Pause-Modus umschalten) |
### Was NICHT funktioniert
diff --git a/alembic/versions/bc1d57524510_add_lessonprogress_table.py b/alembic/versions/bc1d57524510_add_lessonprogress_table.py
new file mode 100644
index 0000000..7e64143
--- /dev/null
+++ b/alembic/versions/bc1d57524510_add_lessonprogress_table.py
@@ -0,0 +1,44 @@
+"""Add LessonProgress table
+
+Revision ID: bc1d57524510
+Revises: 8e747f72e854
+Create Date: 2025-10-29 01:10:19.140020
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'bc1d57524510'
+down_revision: Union[str, Sequence[str], None] = '8e747f72e854'
+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.create_table('lesson_progress',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('lesson_index', sa.Integer(), nullable=False),
+ sa.Column('current_position', sa.Integer(), nullable=True),
+ sa.Column('last_text', sa.Text(), nullable=True),
+ sa.Column('total_elapsed_time', sa.Integer(), nullable=True),
+ sa.Column('key_stroke_count', sa.Integer(), nullable=True),
+ sa.Column('is_paused', sa.Boolean(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_lesson_progress_lesson_index'), 'lesson_progress', ['lesson_index'], unique=True)
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f('ix_lesson_progress_lesson_index'), table_name='lesson_progress')
+ op.drop_table('lesson_progress')
+ # ### end Alembic commands ###
diff --git a/app.py b/app.py
index 31051e6..12862be 100644
--- a/app.py
+++ b/app.py
@@ -8,7 +8,7 @@ from io import StringIO
from typing import Dict, List, Any, Optional
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 models import db, Progress, Statistic, LessonStatistic, LessonProgress, DailyPractice, UserSettings
from datetime import datetime, date, timedelta
# Logging konfigurieren
@@ -228,6 +228,23 @@ def get_or_create_user_settings() -> UserSettings:
logger.info("Neue Benutzereinstellungen erstellt")
return settings
+def get_or_create_lesson_progress(lesson_index: int) -> LessonProgress:
+ """Holt oder erstellt den Fortschritt für eine bestimmte Lektion"""
+ lesson_progress = LessonProgress.query.filter_by(lesson_index=lesson_index).first()
+ if not lesson_progress:
+ lesson_progress = LessonProgress(
+ lesson_index=lesson_index,
+ current_position=0,
+ last_text="",
+ total_elapsed_time=0,
+ key_stroke_count=0,
+ is_paused=True
+ )
+ db.session.add(lesson_progress)
+ db.session.commit()
+ logger.info(f"Neuen Lektions-Fortschritt für Lektion {lesson_index} erstellt")
+ return lesson_progress
+
def save_user_settings(
current_lesson_index: int,
metronome_enabled: bool,
@@ -350,11 +367,11 @@ def welcome():
def train():
"""Hauptseite des Tipptrainers"""
try:
- # Hole den aktuellen Fortschritt aus der Datenbank
+ # Hole den globalen Fortschritt aus der Datenbank (speichert nur current_text_index)
progress = Progress.query.first()
-
+
if not progress:
- # Falls keine Daten vorhanden sind, initialisiere mit leerer Eingabe
+ # Falls keine Daten vorhanden sind, initialisiere
progress = Progress(
current_text_index=0,
current_position=0,
@@ -362,40 +379,50 @@ def train():
)
db.session.add(progress)
db.session.commit()
- logger.info("Neuen Fortschritt initialisiert mit leerer Benutzereingabe")
-
- # Hole den aktuellen Text
- current_text = ""
- if lessons and progress.current_text_index < len(lessons):
- current_text = lessons[progress.current_text_index]['text']
- else:
- # Fallback, falls Index ungültig ist
- current_text = lessons[0]['text'] if lessons else "Keine Lektionen verfügbar"
- progress.current_text_index = 0
- progress.current_position = 0
- progress.last_text = ""
- db.session.commit()
+ logger.info("Neuen globalen Fortschritt initialisiert")
- logger.debug(f"Rendering Index - Current Text Länge: {len(current_text)} Zeichen")
-
# Lade gespeicherte Benutzereinstellungen aus der Datenbank
user_settings = get_or_create_user_settings()
- # Wenn eine gespeicherte Lektion existiert und der Progress noch initial ist,
- # lade die gespeicherte Lektion
- if progress.current_text_index == 0 and user_settings.current_lesson_index > 0:
- progress.current_text_index = user_settings.current_lesson_index
- if lessons and progress.current_text_index < len(lessons):
- current_text = lessons[progress.current_text_index]['text']
- # Setze Position zurück, aber behalte last_text (Benutzereingabe)
- progress.current_position = 0
- db.session.commit()
+ # Bestimme die aktuelle Lektion
+ current_lesson_index = progress.current_text_index
+
+ # Wenn eine gespeicherte Lektion in UserSettings existiert und der Progress noch initial ist,
+ # verwende die gespeicherte Lektion
+ if current_lesson_index == 0 and user_settings.current_lesson_index > 0:
+ current_lesson_index = user_settings.current_lesson_index
+ progress.current_text_index = current_lesson_index
+ db.session.commit()
+
+ # Validiere Lektionsindex
+ if not lessons or current_lesson_index >= len(lessons):
+ current_lesson_index = 0
+ progress.current_text_index = 0
+ db.session.commit()
+
+ # Hole den aktuellen Text
+ current_text = lessons[current_lesson_index]['text'] if lessons else "Keine Lektionen verfügbar"
+
+ # Lade lektionsspezifische Fortschrittsdaten
+ lesson_progress = get_or_create_lesson_progress(current_lesson_index)
+
+ # Daten aus LessonProgress
+ user_input = lesson_progress.last_text
+ cursor_position = lesson_progress.current_position
+ total_elapsed_time = lesson_progress.total_elapsed_time or 0
+ key_stroke_count = lesson_progress.key_stroke_count or 0
+ is_paused = lesson_progress.is_paused if lesson_progress.is_paused is not None else True
+
+ # Netto-Trainingszeit (wird in JavaScript SessionManager verwaltet)
+ net_training_time = total_elapsed_time
+
+ logger.debug(f"Lade Lektion {current_lesson_index}: Position={cursor_position}, Eingabe-Länge={len(user_input)}")
# Bestimme den Lektionstitel
current_lesson_title = "Unbekannte Lektion"
- if lessons and progress.current_text_index < len(lessons):
- lesson = lessons[progress.current_text_index]
- current_lesson_title = lesson.get('title', f"Lektion {progress.current_text_index + 1}")
+ if lessons and current_lesson_index < len(lessons):
+ lesson = lessons[current_lesson_index]
+ current_lesson_title = lesson.get('title', f"Lektion {current_lesson_index + 1}")
# Metronom-Einstellungen aus Datenbank laden
metronome_enabled = user_settings.metronome_enabled
@@ -406,26 +433,13 @@ def train():
target_error_rate = user_settings.target_error_rate
max_bpm_speed = getattr(user_settings, 'max_bpm_speed', 100) # Fallback für Kompatibilität
speed_display = user_settings.speed_display
-
- # Lade die gespeicherte Benutzereingabe und Cursor-Position aus der Datenbank
- user_input = progress.last_text
- cursor_position = progress.current_position
-
- # Lade Session-Daten aus der Datenbank (für Resume nach Neustart/Statistik)
- total_elapsed_time = getattr(progress, 'total_elapsed_time', 0) or 0
- key_stroke_count = getattr(progress, 'key_stroke_count', 0) or 0
- is_paused = getattr(progress, 'is_paused', True)
-
- # Netto-Trainingszeit muss aus total_elapsed_time berechnet werden
- # (wird in JavaScript SessionManager verwaltet)
- net_training_time = total_elapsed_time
# Synchronisiere Session mit DB
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
-
+
return render_template('index.html',
current_text=current_text,
current_position=cursor_position,
@@ -436,7 +450,7 @@ def train():
is_paused=is_paused,
lessons=lessons,
current_lesson_title=current_lesson_title,
- current_lesson_index=progress.current_text_index,
+ current_lesson_index=current_lesson_index,
metronome_enabled=metronome_enabled,
metronome_bpm=metronome_bpm,
metronome_sound=metronome_sound,
@@ -596,40 +610,74 @@ def update_progress():
return jsonify({'error': 'Cursor-Position muss >= 0 sein'}), 400
is_paused = bool(data.get('is_paused', False))
-
- # Hole den aktuellen Fortschritt
+
+ # Hole den globalen Fortschritt (für lesson_index)
progress = Progress.query.first()
- if progress:
- try:
- # Aktualisiere den Fortschritt
- progress.current_position = cursor_position
- progress.last_text = user_input
+ if not progress:
+ return jsonify({'error': 'Progress nicht gefunden'}), 400
- # Speichere Session-Daten auch in der DB (für Resume nach Neustart/Statistik)
- progress.total_elapsed_time = int(total_elapsed_time)
- progress.key_stroke_count = key_stroke_count
- progress.is_paused = is_paused
+ try:
+ # Hole lektionsspezifischen Fortschritt
+ lesson_index = progress.current_text_index
+ lesson_progress = get_or_create_lesson_progress(lesson_index)
- # Speichere in der Datenbank
- db.session.commit()
+ # Aktualisiere lektionsspezifische Daten
+ lesson_progress.current_position = cursor_position
+ lesson_progress.last_text = user_input
+ lesson_progress.total_elapsed_time = int(total_elapsed_time)
+ lesson_progress.key_stroke_count = key_stroke_count
+ lesson_progress.is_paused = is_paused
- # Speichere zusätzliche Zustandsdaten auch in der Session (für Kompatibilität)
- 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
+ # Prüfe, ob die Lektion abgeschlossen ist
+ lesson_completed = cursor_position >= len(current_text) and len(user_input) == len(current_text)
- # Berechne Statistiken mit echter Zeit
+ if lesson_completed:
+ # Speichere Statistik für abgeschlossene Lektion
+ logger.info(f"Lektion {lesson_index} abgeschlossen! Speichere Statistik...")
+
+ # Berechne finale Statistiken
stats = calculate_statistics(user_input, current_text, elapsed_time)
- return jsonify(stats)
- except Exception as db_error:
- db.session.rollback()
- logger.error(f"Datenbank-Fehler beim Speichern des Fortschritts: {db_error}", exc_info=True)
- return jsonify({'error': 'Fehler beim Speichern des Fortschritts'}), 500
- else:
- return jsonify({'error': 'Progress nicht gefunden'}), 400
+ # Speichere in LessonStatistic
+ lesson_stat = LessonStatistic(
+ lesson_index=lesson_index,
+ chars_per_minute=stats['typing_speed'],
+ error_rate=stats['error_rate'],
+ wpm=stats['words_per_minute'],
+ training_duration=total_elapsed_time / 1000.0 # Millisekunden zu Sekunden
+ )
+ db.session.add(lesson_stat)
+
+ # Setze LessonProgress zurück
+ lesson_progress.current_position = 0
+ lesson_progress.last_text = ""
+ lesson_progress.total_elapsed_time = 0
+ lesson_progress.key_stroke_count = 0
+ lesson_progress.is_paused = True
+
+ logger.info(f"Lektion {lesson_index} zurückgesetzt")
+
+ # Speichere in der Datenbank
+ db.session.commit()
+
+ # Speichere zusätzliche Zustandsdaten auch in der Session (für Kompatibilität)
+ 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
+
+ # Berechne Statistiken mit echter Zeit
+ stats = calculate_statistics(user_input, current_text, elapsed_time)
+
+ # Füge lesson_completed zu Response hinzu
+ stats['lesson_completed'] = lesson_completed
+
+ return jsonify(stats)
+ except Exception as db_error:
+ db.session.rollback()
+ logger.error(f"Datenbank-Fehler beim Speichern des Fortschritts: {db_error}", exc_info=True)
+ return jsonify({'error': 'Fehler beim Speichern des Fortschritts'}), 500
except Exception as e:
logger.error(f"Fehler in update_progress: {e}", exc_info=True)
@@ -640,31 +688,36 @@ def calculate_statistics(user_input: str, current_text: str, elapsed_time: float
correct_chars = 0
incorrect_chars = 0
total_chars = len(user_input)
-
+
# Zähle korrekte und falsche Zeichen
for i, char in enumerate(user_input):
if i < len(current_text) and char == current_text[i]:
correct_chars += 1
else:
incorrect_chars += 1
-
+
# Berechne Metriken basierend auf echter Zeit
- if total_chars > 0 and elapsed_time > 0:
+ # Mindestzeit von 1 Sekunde, um unrealistische Werte zu vermeiden
+ if total_chars > 0 and elapsed_time >= 1.0:
# Zeichen pro Minute (ganzzahlig)
chars_per_minute = (total_chars / elapsed_time) * 60
# Wörter pro Minute gemäß Formel: (Gesamtzahl der korrekt getippten Zeichen/5)/((Zeit in Sekunden)/60)
words_per_minute = (correct_chars / 5) / (elapsed_time / 60)
# Durchschnittliche Zeit pro Zeichen
duration_per_char = elapsed_time / total_chars
+
+ # Begrenze auf realistische Maximalwerte (z.B. max 1000 ZPM)
+ chars_per_minute = min(chars_per_minute, 1000)
+ words_per_minute = min(words_per_minute, 200)
else:
chars_per_minute = 0
words_per_minute = 0
duration_per_char = 0
-
+
# Fehlerrate und Genauigkeit
error_rate = (incorrect_chars / total_chars * 100) if total_chars > 0 else 0
accuracy = (correct_chars / total_chars * 100) if total_chars > 0 else 0
-
+
return {
'correct_chars': correct_chars,
'incorrect_chars': incorrect_chars,
@@ -681,46 +734,50 @@ def next_text():
"""Wechselt zum nächsten Text"""
try:
progress = Progress.query.first()
-
+
if not progress:
return jsonify({'error': 'Progress nicht gefunden'}), 400
-
+
if not lessons:
return jsonify({'error': 'Keine Lektionen verfügbar'}), 400
-
+
# Wechsel zum nächsten Text
+ old_lesson_index = progress.current_text_index
+
if progress.current_text_index < len(lessons) - 1:
progress.current_text_index += 1
else:
# Wenn wir am Ende sind, starten wir von vorne
progress.current_text_index = 0
- progress.current_position = 0
- progress.last_text = ""
-
- # Setze Session-Daten zurück bei Lektionswechsel
- progress.total_elapsed_time = 0
- progress.key_stroke_count = 0
- progress.is_paused = True
-
+ new_lesson_index = progress.current_text_index
db.session.commit()
- # Synchronisiere Session
- session['total_elapsed_time'] = 0
- session['net_training_time'] = 0
- session['key_stroke_count'] = 0
- session['is_paused'] = True
+ # Lade LessonProgress für neue Lektion (wird automatisch erstellt falls nicht vorhanden)
+ lesson_progress = get_or_create_lesson_progress(new_lesson_index)
+
+ # Synchronisiere Session mit der neuen Lektion
+ session['total_elapsed_time'] = lesson_progress.total_elapsed_time
+ session['net_training_time'] = lesson_progress.total_elapsed_time
+ session['key_stroke_count'] = lesson_progress.key_stroke_count
+ session['is_paused'] = lesson_progress.is_paused
# Bestimme den Titel der neuen Lektion
- new_lesson = lessons[progress.current_text_index]
- lesson_title = new_lesson.get('title', f"Lektion {progress.current_text_index + 1}")
+ new_lesson = lessons[new_lesson_index]
+ lesson_title = new_lesson.get('title', f"Lektion {new_lesson_index + 1}")
+
+ logger.info(f"Lektionswechsel: {old_lesson_index} -> {new_lesson_index}")
return jsonify({
'text': new_lesson['text'],
- 'index': progress.current_text_index,
+ 'index': new_lesson_index,
'total': len(lessons),
'title': lesson_title,
- 'is_paused': True
+ 'current_position': lesson_progress.current_position,
+ 'last_text': lesson_progress.last_text,
+ 'total_elapsed_time': lesson_progress.total_elapsed_time,
+ 'key_stroke_count': lesson_progress.key_stroke_count,
+ 'is_paused': lesson_progress.is_paused
})
except Exception as e:
@@ -733,42 +790,46 @@ def set_lesson():
try:
data = request.get_json()
lesson_index = data.get('lesson_index')
-
+
if lesson_index is None or not isinstance(lesson_index, int):
return jsonify({'error': 'Ungültiger Lektionsindex'}), 400
-
+
if lesson_index < 0 or lesson_index >= len(lessons):
return jsonify({'error': 'Lektionsindex außerhalb des gültigen Bereichs'}), 400
-
+
progress = Progress.query.first()
if not progress:
return jsonify({'error': 'Progress nicht gefunden'}), 400
-
+
+ old_lesson_index = progress.current_text_index
+
+ # Setze neue Lektion
progress.current_text_index = lesson_index
- progress.current_position = 0
- progress.last_text = ""
-
- # Setze Session-Daten zurück bei Lektionswechsel
- progress.total_elapsed_time = 0
- progress.key_stroke_count = 0
- progress.is_paused = True
-
db.session.commit()
- # Synchronisiere Session
- session['total_elapsed_time'] = 0
- session['net_training_time'] = 0
- session['key_stroke_count'] = 0
- session['is_paused'] = True
+ # Lade LessonProgress für neue Lektion (wird automatisch erstellt falls nicht vorhanden)
+ lesson_progress = get_or_create_lesson_progress(lesson_index)
+
+ # Synchronisiere Session mit der neuen Lektion
+ session['total_elapsed_time'] = lesson_progress.total_elapsed_time
+ session['net_training_time'] = lesson_progress.total_elapsed_time
+ session['key_stroke_count'] = lesson_progress.key_stroke_count
+ session['is_paused'] = lesson_progress.is_paused
# Bestimme den Titel der neuen Lektion
new_lesson = lessons[lesson_index]
lesson_title = new_lesson.get('title', f"Lektion {lesson_index + 1}")
+ logger.info(f"Lektionswechsel: {old_lesson_index} -> {lesson_index}")
+
return jsonify({
'text': new_lesson['text'],
'title': lesson_title,
- 'is_paused': True
+ 'current_position': lesson_progress.current_position,
+ 'last_text': lesson_progress.last_text,
+ 'total_elapsed_time': lesson_progress.total_elapsed_time,
+ 'key_stroke_count': lesson_progress.key_stroke_count,
+ 'is_paused': lesson_progress.is_paused
})
except Exception as e:
@@ -835,6 +896,80 @@ def save_lesson_statistics():
logger.error(f"Fehler in save_lesson_statistics: {e}", exc_info=True)
return jsonify({'error': 'Interner Serverfehler'}), 500
+@app.route('/reset_lesson', methods=['POST'])
+def reset_lesson():
+ """Setzt den Fortschritt der aktuellen Lektion zurück"""
+ try:
+ # Hole den globalen Fortschritt
+ progress = Progress.query.first()
+ if not progress:
+ return jsonify({'error': 'Progress nicht gefunden'}), 400
+
+ lesson_index = progress.current_text_index
+
+ # Hole oder erstelle LessonProgress für aktuelle Lektion
+ lesson_progress = get_or_create_lesson_progress(lesson_index)
+
+ # Setze alle Werte zurück
+ lesson_progress.current_position = 0
+ lesson_progress.last_text = ""
+ lesson_progress.total_elapsed_time = 0
+ lesson_progress.key_stroke_count = 0
+ lesson_progress.is_paused = True
+
+ db.session.commit()
+
+ logger.info(f"Lektion {lesson_index} wurde zurückgesetzt")
+
+ return jsonify({
+ 'status': 'success',
+ 'message': 'Lektion zurückgesetzt',
+ 'lesson_index': lesson_index
+ })
+
+ except Exception as e:
+ db.session.rollback()
+ logger.error(f"Fehler in reset_lesson: {e}", exc_info=True)
+ return jsonify({'error': 'Interner Serverfehler'}), 500
+
+@app.route('/reset_lesson_statistics', methods=['POST'])
+def reset_lesson_statistics():
+ """Löscht alle Statistiken für eine bestimmte Lektion"""
+ try:
+ data = request.get_json()
+ if not data:
+ return jsonify({'error': 'Keine Daten empfangen'}), 400
+
+ lesson_index = data.get('lesson_index')
+ if lesson_index is None:
+ return jsonify({'error': 'Lektionsindex fehlt'}), 400
+
+ try:
+ lesson_index = int(lesson_index)
+ except (ValueError, TypeError):
+ return jsonify({'error': 'Ungültiger Lektionsindex'}), 400
+
+ if lesson_index < 0 or lesson_index >= len(lessons):
+ return jsonify({'error': 'Lektionsindex außerhalb des gültigen Bereichs'}), 400
+
+ # Lösche alle Statistiken für diese Lektion
+ deleted_count = LessonStatistic.query.filter_by(lesson_index=lesson_index).delete()
+ db.session.commit()
+
+ logger.info(f"{deleted_count} Statistik-Einträge für Lektion {lesson_index} gelöscht")
+
+ return jsonify({
+ 'status': 'success',
+ 'message': f'{deleted_count} Statistik-Einträge gelöscht',
+ 'lesson_index': lesson_index,
+ 'deleted_count': deleted_count
+ })
+
+ except Exception as e:
+ db.session.rollback()
+ logger.error(f"Fehler in reset_lesson_statistics: {e}", exc_info=True)
+ return jsonify({'error': 'Interner Serverfehler'}), 500
+
@app.route('/statistics')
def statistics():
"""Zeigt die Statistik-Seite mit Diagrammen"""
diff --git a/models.py b/models.py
index aa513fe..c8818af 100644
--- a/models.py
+++ b/models.py
@@ -14,6 +14,21 @@ class Progress(db.Model):
key_stroke_count = db.Column(db.Integer, default=0)
is_paused = db.Column(db.Boolean, default=True)
+class LessonProgress(db.Model):
+ """Speichert Fortschritt und Session-Daten für jede einzelne Lektion"""
+ id = db.Column(db.Integer, primary_key=True)
+ lesson_index = db.Column(db.Integer, nullable=False, unique=True, index=True)
+ current_position = db.Column(db.Integer, default=0)
+ last_text = db.Column(db.Text, default='')
+
+ # Session-Daten pro Lektion
+ total_elapsed_time = db.Column(db.Integer, default=0) # in Millisekunden
+ key_stroke_count = db.Column(db.Integer, default=0)
+ is_paused = db.Column(db.Boolean, default=True)
+
+ # Zeitstempel
+ updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())
+
class Statistic(db.Model):
id = db.Column(db.Integer, primary_key=True)
total_chars = db.Column(db.Integer, default=0)
diff --git a/static/js/main.js b/static/js/main.js
index d9112bc..44249fa 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -162,6 +162,13 @@ class TypewriterApp {
setupEventListeners() {
// Keyboard events
document.addEventListener('keydown', (event) => {
+ // CTRL-P zum Pausieren/Fortsetzen
+ if (event.ctrlKey && event.key === 'p') {
+ event.preventDefault();
+ this.togglePause();
+ return;
+ }
+
this.keyboardHandler.handleKeyDown(event, {
userInput: this.userInput,
currentCharIndex: this.currentCharIndex,
@@ -170,6 +177,15 @@ class TypewriterApp {
});
});
+ // Speichere beim Verlassen der Seite
+ window.addEventListener('beforeunload', (event) => {
+ if (!this.sessionManager.isPaused) {
+ // Pausiere und speichere
+ this.sessionManager.pause();
+ this.saveProgress();
+ }
+ });
+
// Volume toggle
const volumeToggle = document.getElementById('volume-toggle');
if (volumeToggle) {
@@ -218,6 +234,12 @@ class TypewriterApp {
});
}
+ // Reset lesson button
+ const resetLessonButton = document.getElementById('reset-lesson-button');
+ if (resetLessonButton) {
+ resetLessonButton.addEventListener('click', () => this.resetLesson());
+ }
+
// Next button
const nextBtn = document.getElementById('next-btn');
if (nextBtn) {
@@ -403,6 +425,13 @@ class TypewriterApp {
*/
async saveProgress() {
try {
+ // Pausiere vor dem Speichern
+ if (!this.sessionManager.isPaused) {
+ this.sessionManager.pause();
+ this.uiManager.togglePlayPause(true); // true = paused
+ this.metronomePlayer.stop();
+ }
+
await this.sessionManager.saveProgress(
this.userInput,
this.currentCharIndex,
@@ -413,6 +442,25 @@ class TypewriterApp {
}
}
+ /**
+ * Toggle pause/resume
+ */
+ togglePause() {
+ this.sessionManager.togglePause();
+ this.uiManager.togglePlayPause(this.sessionManager.isPaused);
+
+ if (this.sessionManager.isPaused) {
+ this.metronomePlayer.stop();
+ } else {
+ if (this.metronomePlayer.enabled) {
+ this.metronomePlayer.start();
+ }
+ }
+
+ this.updateStatistics();
+ this.updateTrainingTimeDisplay();
+ }
+
/**
* Save metronome settings
*/
@@ -499,11 +547,16 @@ class TypewriterApp {
try {
const data = await this.sessionManager.nextLesson();
+ // Lade lektionsspezifische Daten aus Response
this.fullText = data.text;
- this.currentCharIndex = 0;
- this.userInput = '';
- this.sessionManager.reset();
- this.sessionManager.pause();
+ this.currentCharIndex = data.current_position || 0;
+ this.userInput = data.last_text || '';
+
+ // Setze Session-Daten aus der neuen Lektion
+ this.sessionManager.totalElapsedTime = data.total_elapsed_time || 0;
+ this.sessionManager.netTrainingTime = data.total_elapsed_time || 0;
+ this.sessionManager.keyStrokeCount = data.key_stroke_count || 0;
+ this.sessionManager.isPaused = data.is_paused !== undefined ? data.is_paused : true;
// Stoppe Metronom
this.metronomePlayer.stop();
@@ -517,6 +570,7 @@ class TypewriterApp {
this.updateDisplay();
this.updateStatistics();
+ this.updateTrainingTimeDisplay();
} catch (error) {
this.uiManager.alert('Fehler beim Laden des nächsten Textes: ' + error.message);
}
@@ -529,11 +583,16 @@ class TypewriterApp {
try {
const data = await this.sessionManager.setLesson(lessonIndex);
+ // Lade lektionsspezifische Daten aus Response
this.fullText = data.text;
- this.currentCharIndex = 0;
- this.userInput = '';
- this.sessionManager.reset();
- this.sessionManager.pause();
+ this.currentCharIndex = data.current_position || 0;
+ this.userInput = data.last_text || '';
+
+ // Setze Session-Daten aus der neuen Lektion
+ this.sessionManager.totalElapsedTime = data.total_elapsed_time || 0;
+ this.sessionManager.netTrainingTime = data.total_elapsed_time || 0;
+ this.sessionManager.keyStrokeCount = data.key_stroke_count || 0;
+ this.sessionManager.isPaused = data.is_paused !== undefined ? data.is_paused : true;
// Stoppe Metronom
this.metronomePlayer.stop();
@@ -547,6 +606,36 @@ class TypewriterApp {
this.updateDisplay();
this.updateStatistics();
+ this.updateTrainingTimeDisplay();
+ } catch (error) {
+ this.uiManager.alert('Fehler: ' + error.message);
+ }
+ }
+
+ /**
+ * Reset current lesson
+ */
+ async resetLesson() {
+ const confirmed = confirm('Möchten Sie den Fortschritt dieser Lektion wirklich zurücksetzen?');
+ if (!confirmed) {
+ return;
+ }
+
+ try {
+ const response = await fetch('/reset_lesson', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Fehler beim Zurücksetzen der Lektion');
+ }
+
+ // Seite neu laden, um zurückgesetzte Daten anzuzeigen
+ window.location.reload();
} catch (error) {
this.uiManager.alert('Fehler: ' + error.message);
}
diff --git a/static/js/ui.js b/static/js/ui.js
index 7c64baa..6633d8a 100644
--- a/static/js/ui.js
+++ b/static/js/ui.js
@@ -190,10 +190,11 @@ export class UIManager {
*/
setupStatisticsLink(saveCallback) {
document.querySelectorAll('.statistics-link').forEach(link => {
- link.addEventListener('click', () => {
+ link.addEventListener('click', async (event) => {
+ event.preventDefault();
console.log('Statistics link clicked');
if (saveCallback) {
- saveCallback();
+ await saveCallback();
}
window.location.href = '/statistics';
});
diff --git a/templates/index.html b/templates/index.html
index b3f79b0..2d7f103 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -28,6 +28,13 @@
type="button">
+