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:
parent
6cebc245be
commit
14b8a2040d
8 changed files with 465 additions and 124 deletions
1
HILFE.md
1
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
|
||||
|
||||
|
|
|
|||
44
alembic/versions/bc1d57524510_add_lessonprogress_table.py
Normal file
44
alembic/versions/bc1d57524510_add_lessonprogress_table.py
Normal 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
365
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"""
|
||||
|
|
|
|||
15
models.py
15
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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue