Lektionsspezifische Datenpersistenz und automatisches Pausieren beim Seitenwechsel

- Neue LessonProgress-Tabelle für unabhängige Fortschrittsspeicherung pro Lektion
- Alembic-Migration für LessonProgress-Tabelle erstellt
- Automatisches Speichern und Pausieren beim Wechsel zur Statistikseite
- Korrekte Wiederherstellung aller Daten beim Zurückkehren zur Trainingsseite
- Reset-Buttons (refresh-ccw) für Lektions-Fortschritt und Statistiken
- CTRL-P Tastenkombination zum Pausieren/Fortsetzen hinzugefügt
- Fix: Unrealistische ZPM-Werte durch Mindestzeit von 1 Sekunde verhindert
- Lektionswechsel laden gespeicherte Fortschrittsdaten der jeweiligen Lektion
- Automatische Statistik-Speicherung bei Lektion-Abschluss

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jamulix 2025-10-29 03:51:25 +01:00
commit 14b8a2040d
8 changed files with 465 additions and 124 deletions

View file

@ -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

View file

@ -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 ###

365
app.py
View file

@ -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"""

View file

@ -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)

View file

@ -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);
}

View file

@ -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';
});

View file

@ -28,6 +28,13 @@
type="button">
<i data-lucide="settings" style="width: 24px; height: 24px;" aria-hidden="true"></i>
</button>
<button id="reset-lesson-button"
style="width: 24px; height: 24px; cursor: pointer; border: none; background: none; padding: 0; color: white;"
aria-label="Lektion zurücksetzen"
title="Lektion zurücksetzen"
type="button">
<i data-lucide="refresh-ccw" style="width: 24px; height: 24px;" aria-hidden="true"></i>
</button>
<button id="theme-toggle"
style="width: 24px; height: 24px; cursor: pointer; position: relative; border: none; background: none; padding: 0; color: white;"
aria-label="Zwischen hellem und dunklem Design wechseln"

View file

@ -26,6 +26,9 @@
<i data-lucide="sun" id="theme-toggle-sun" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0;"></i>
<i data-lucide="moon" id="theme-toggle-moon" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0; display: none;"></i>
</div>
<div id="reset-statistics-button" style="width: 24px; height: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center;" aria-label="Statistiken für diese Lektion löschen" title="Statistiken für diese Lektion löschen">
<i data-lucide="refresh-ccw" style="width: 24px; height: 24px;"></i>
</div>
<div onclick="window.location.href='/train'" style="width: 24px; height: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center;" aria-label="Zurück zum Training" title="Zurück zum Training">
<i data-lucide="undo-2" style="width: 24px; height: 24px;"></i>
</div>
@ -283,9 +286,55 @@
});
}
function resetLessonStatistics() {
// Hole die aktuell ausgewählte Lektion
const selectedLink = document.querySelector('.lesson-link.fw-bold.text-danger');
if (!selectedLink) {
alert('Keine Lektion ausgewählt');
return;
}
const lessonIndex = parseInt(selectedLink.getAttribute('data-lesson-index'));
const lessonNumber = selectedLink.textContent.trim();
const confirmed = confirm(`Möchten Sie alle Statistiken für Lektion ${lessonNumber} wirklich löschen?`);
if (!confirmed) {
return;
}
fetch('/reset_lesson_statistics', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({ lesson_index: lessonIndex })
})
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Fehler: ' + data.error);
} else {
alert(data.message);
// Seite neu laden, um die Änderungen anzuzeigen
window.location.reload();
}
})
.catch(error => {
console.error('Fehler:', error);
alert('Fehler beim Löschen der Statistiken');
});
}
// Lade initial die Statistiken für die aktuelle Lektion
document.addEventListener('DOMContentLoaded', function() {
loadLessonStatistics({{ current_lesson_index }});
// Event-Handler für Reset-Button
const resetButton = document.getElementById('reset-statistics-button');
if (resetButton) {
resetButton.addEventListener('click', resetLessonStatistics);
}
});
</script>
</body>