From 14b8a2040dc5b05998cb1a067023f44af2a78f4a Mon Sep 17 00:00:00 2001 From: jamulix Date: Wed, 29 Oct 2025 03:51:25 +0100 Subject: [PATCH] Lektionsspezifische Datenpersistenz und automatisches Pausieren beim Seitenwechsel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- HILFE.md | 1 + .../bc1d57524510_add_lessonprogress_table.py | 44 +++ app.py | 365 ++++++++++++------ models.py | 15 + static/js/main.js | 105 ++++- static/js/ui.js | 5 +- templates/index.html | 7 + templates/statistics.html | 49 +++ 8 files changed, 466 insertions(+), 125 deletions(-) create mode 100644 alembic/versions/bc1d57524510_add_lessonprogress_table.py 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"> +