diff --git a/app.py b/app.py index 1a49e15..a288563 100644 --- a/app.py +++ b/app.py @@ -1,12 +1,13 @@ import os import json import re -from flask import Flask, render_template, request, jsonify +from flask import Flask, render_template, request, jsonify, session from models import db, Progress, Statistic, LessonStatistic app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///typewriter.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.secret_key = 'your-secret-key-here' # Für Session verwenden db.init_app(app) # Fallback-Lektionen für den Fall, dass die JSON-Datei nicht geladen werden kann @@ -229,21 +230,33 @@ def index(): lesson = lessons[progress.current_text_index] current_lesson_title = lesson.get('title', f"Lektion {progress.current_text_index + 1}") + # Metronom-Einstellungen aus Session laden oder Standardwerte setzen + metronome_enabled = session.get('metronome_enabled', False) + metronome_bpm = session.get('metronome_bpm', 60) + metronome_sound = session.get('metronome_sound', 'beep') + return render_template('index.html', current_text=current_text, current_position=progress.current_position, last_text=progress.last_text, lessons=lessons, current_lesson_title=current_lesson_title, - current_lesson_index=progress.current_text_index) + current_lesson_index=progress.current_text_index, + metronome_enabled=metronome_enabled, + metronome_bpm=metronome_bpm, + metronome_sound=metronome_sound) except Exception as e: print(f"Fehler in index(): {e}") + # Auch im Fehlerfall Metronom-Standardwerte übergeben return render_template('index.html', current_text="Fehler beim Laden der Lektionen", current_position=0, last_text="", - lessons=[]) + lessons=[], + metronome_enabled=False, + metronome_bpm=60, + metronome_sound='beep') @app.route('/debug') def debug(): @@ -569,6 +582,19 @@ def get_lesson_statistics(lesson_index): print(f"Fehler in get_lesson_statistics: {e}") return jsonify({'error': 'Interner Serverfehler'}), 500 +@app.route('/set_metronome', methods=['POST']) +def set_metronome(): + """Setzt die Metronom-Einstellungen in der Session""" + try: + data = request.get_json() + session['metronome_enabled'] = data.get('enabled', False) + session['metronome_bpm'] = data.get('bpm', 60) + session['metronome_sound'] = data.get('sound', 'beep') + return jsonify({'status': 'success'}) + except Exception as e: + print(f"Fehler in set_metronome: {e}") + return jsonify({'error': 'Interner Serverfehler'}), 500 + @app.route('/end_session', methods=['POST']) def end_session(): """Beendet eine Tipp-Session und speichert Statistiken""" diff --git a/static/css/style.css b/static/css/style.css index 9cdf7a4..b4761ff 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -29,6 +29,18 @@ body { } } +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(0, 150, 255, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(0, 150, 255, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(0, 150, 255, 0); + } +} + /* Dark Theme Anpassungen für Cursor */ [data-bs-theme="dark"] .cursor-char::after { background-color: #0cf; diff --git a/static/js/script.js b/static/js/script.js index b7041f2..e25c846 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -15,23 +15,244 @@ function toggleTheme() { } function updateThemeButton(theme) { - const button = document.getElementById('theme-toggle'); + const sunIcon = document.getElementById('theme-toggle-sun'); + const moonIcon = document.getElementById('theme-toggle-moon'); if (theme === 'dark') { - button.innerHTML = ''; - button.classList.remove('btn-outline-light'); - button.classList.add('btn-outline-warning'); + sunIcon.style.display = 'none'; + moonIcon.style.display = 'block'; } else { - button.innerHTML = ''; - button.classList.remove('btn-outline-warning'); - button.classList.add('btn-outline-light'); + sunIcon.style.display = 'block'; + moonIcon.style.display = 'none'; } } document.addEventListener('DOMContentLoaded', function() { + // Lucide Icons initialisieren + lucide.createIcons(); + // Theme initialisieren initializeTheme(); document.getElementById('theme-toggle').addEventListener('click', toggleTheme); + // Aktuelle Textdaten - mit Fallback falls undefiniert + let fullText = window.fullText || "Kein Text verfügbar. Bitte laden Sie die Seite neu."; + let currentCharIndex = window.currentCharIndex || 0; + let lastStartTime = null; + let totalElapsedTime = 0; + let isPaused = false; + let timerInterval = null; + let userInput = ''; // Gesamte Eingabe + let keyStrokeCount = 0; // Zähler für alle Tastaturanschläge (inkl. Backspace) + let lines = fullText.split('\n'); + let userScrolled = false; // Flag für manuelles Scrollen + + // Volume-Toggle initialisieren - Standardmäßig Ton an (nicht stumm) + let metronomeMuted = false; + const volumeOn = document.getElementById('volume-on'); + const volumeOff = document.getElementById('volume-off'); + // Initiale Anzeige: volume-2 anzeigen, volume-x verstecken + volumeOn.style.display = 'block'; + volumeOff.style.display = 'none'; + + document.getElementById('volume-toggle').addEventListener('click', function() { + metronomeMuted = !metronomeMuted; + if (metronomeMuted) { + volumeOn.style.display = 'none'; + volumeOff.style.display = 'block'; + } else { + volumeOn.style.display = 'block'; + volumeOff.style.display = 'none'; + } + }); + + // ErrorLock State - Standardmäßig geöffnet (errorLock = false) + let errorLock = false; + const lockClosed = document.getElementById('lock-closed'); + const lockOpen = document.getElementById('lock-open'); + // Initiale Anzeige: offenes Schloss anzeigen, geschlossenes verstecken + lockClosed.style.display = 'none'; + lockOpen.style.display = 'block'; + + // Metronom State - Initialisierung mit Werten von Flask + let metronomeEnabled = window.metronomeEnabled || false; + let metronomeInterval = null; + let audioContext = null; + let metronomeSound = window.metronomeSound || 'beep'; + let metronomeBPM = window.metronomeBPM || 60; + const metronomeToggle = document.getElementById('metronome-toggle'); + const metronomeOn = document.getElementById('metronome-on'); + const metronomeOff = document.getElementById('metronome-off'); + + // Metronom-Funktionen + function createBeepSound(vol, freq, duration) { + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + gainNode.gain.value = vol; + oscillator.frequency.value = freq; + oscillator.type = 'sine'; + + oscillator.start(); + setTimeout(function() { + oscillator.stop(); + }, duration); + } + + function createClickSound() { + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + const bufferSize = audioContext.sampleRate * 0.01; + const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate); + const data = buffer.getChannelData(0); + + for (let i = 0; i < bufferSize; i++) { + data[i] = Math.random() * 2 - 1; + } + + const source = audioContext.createBufferSource(); + source.buffer = buffer; + + const gainNode = audioContext.createGain(); + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.1); + + source.connect(gainNode); + gainNode.connect(audioContext.destination); + source.start(); + } + + function playMetronomeSound() { + // Wenn der Ton stummgeschaltet ist, keinen Sound abspielen + if (metronomeMuted) { + // Aber visuelles Feedback trotzdem anzeigen + const cursorElement = document.querySelector('.cursor-char'); + if (cursorElement) { + cursorElement.style.animation = 'pulse 0.3s'; + setTimeout(() => { + cursorElement.style.animation = ''; + }, 300); + } + return; + } + + if (metronomeSound === 'beep') { + createBeepSound(0.1, 800, 50); + } else { + createClickSound(); + } + + // Visuelles Feedback: Cursor pulsieren lassen + const cursorElement = document.querySelector('.cursor-char'); + if (cursorElement) { + cursorElement.style.animation = 'pulse 0.3s'; + setTimeout(() => { + cursorElement.style.animation = ''; + }, 300); + } + } + + function startMetronome() { + stopMetronome(); + const interval = (60 / metronomeBPM) * 1000; + playMetronomeSound(); // Sofort abspielen + metronomeInterval = setInterval(playMetronomeSound, interval); + } + + function stopMetronome() { + if (metronomeInterval) { + clearInterval(metronomeInterval); + metronomeInterval = null; + } + } + + // Initialen Zustand des Metronom-Toggles setzen + if (metronomeEnabled) { + metronomeOn.style.display = 'block'; + metronomeOff.style.display = 'none'; + if (!isPaused) { + startMetronome(); + } + } else { + metronomeOn.style.display = 'none'; + metronomeOff.style.display = 'block'; + } + + // Lock-Toggle initialisieren + document.getElementById('lock-toggle').addEventListener('click', function() { + if (errorLock) { + // Wechsel zu unlocked + errorLock = false; + lockClosed.style.display = 'none'; + lockOpen.style.display = 'block'; + } else { + // Wechsel zu locked + errorLock = true; + lockClosed.style.display = 'block'; + lockOpen.style.display = 'none'; + } + }); + + // Metronom-Toggle initialisieren + metronomeToggle.addEventListener('click', function() { + metronomeEnabled = !metronomeEnabled; + if (metronomeEnabled) { + metronomeOn.style.display = 'block'; + metronomeOff.style.display = 'none'; + startMetronome(); + } else { + metronomeOn.style.display = 'none'; + metronomeOff.style.display = 'block'; + stopMetronome(); + } + // Einstellungen an Server senden + fetch('/set_metronome', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + enabled: metronomeEnabled, + bpm: metronomeBPM, + sound: metronomeSound + }) + }); + }); + + // Play/Pause-Toggle initialisieren + const playPauseToggle = document.getElementById('play-pause-toggle'); + if (playPauseToggle) { + playPauseToggle.addEventListener('click', function() { + if (isPaused) { + // Fortsetzen + isPaused = false; + lastStartTime = new Date(); + document.getElementById('play-pause-pause').style.display = 'block'; + document.getElementById('play-pause-play').style.display = 'none'; + if (metronomeEnabled) { + startMetronome(); + } + } else { + // Pausieren + isPaused = true; + if (lastStartTime !== null) { + totalElapsedTime += (new Date() - lastStartTime); + lastStartTime = null; + } + document.getElementById('play-pause-pause').style.display = 'none'; + document.getElementById('play-pause-play').style.display = 'block'; + stopMetronome(); + } + updateStatistics(); + }); + } + const targetTextElement = document.getElementById('target-text'); const nextBtn = document.getElementById('next-btn'); const endBtn = document.getElementById('end-btn'); @@ -44,16 +265,6 @@ document.addEventListener('DOMContentLoaded', function() { const totalElement = document.getElementById('total'); const wpmElement = document.getElementById('wpm'); - // Aktuelle Textdaten - let fullText = window.fullText; - let currentCharIndex = window.currentCharIndex; - let startTime = null; - let timerInterval = null; - let userInput = ''; // Gesamte Eingabe - let keyStrokeCount = 0; // Zähler für alle Tastaturanschläge (inkl. Backspace) - let lines = fullText.split('\n'); - let userScrolled = false; // Flag für manuelles Scrollen - // Setze Fokus auf das Dokument, um Tastatureingaben zu erfassen document.addEventListener('click', function() { document.body.focus(); @@ -109,13 +320,20 @@ document.addEventListener('DOMContentLoaded', function() { return; } - // Behandle Pfeiltasten + // Im Pause-Modus sind keine Tasten erlaubt (Cursor-Position bleibt erhalten) + if (isPaused) { + // Blockiere alle Tasten im Pause-Modus + event.preventDefault(); + return; + } + + // Behandle Pfeiltasten (nur im Play-Modus) if (event.key === 'ArrowLeft') { event.preventDefault(); if (currentCharIndex > 0) { keyStrokeCount++; - if (!startTime) { - startTime = new Date(); + if (!lastStartTime && !isPaused) { + lastStartTime = new Date(); } currentCharIndex--; updateTextDisplay(); @@ -128,8 +346,8 @@ document.addEventListener('DOMContentLoaded', function() { event.preventDefault(); if (currentCharIndex < userInput.length) { keyStrokeCount++; - if (!startTime) { - startTime = new Date(); + if (!lastStartTime && !isPaused) { + lastStartTime = new Date(); } currentCharIndex++; updateTextDisplay(); @@ -143,8 +361,8 @@ document.addEventListener('DOMContentLoaded', function() { event.preventDefault(); if (userInput.length > 0 && currentCharIndex > 0) { keyStrokeCount++; - if (!startTime) { - startTime = new Date(); + if (!lastStartTime && !isPaused) { + lastStartTime = new Date(); } userInput = userInput.slice(0, currentCharIndex - 1) + userInput.slice(currentCharIndex); currentCharIndex--; @@ -161,8 +379,8 @@ document.addEventListener('DOMContentLoaded', function() { userInput = userInput.slice(0, currentCharIndex) + '\n' + userInput.slice(currentCharIndex); currentCharIndex++; keyStrokeCount++; - if (!startTime) { - startTime = new Date(); + if (!lastStartTime && !isPaused) { + lastStartTime = new Date(); } updateTextDisplay(); updateStatistics(); @@ -175,6 +393,12 @@ document.addEventListener('DOMContentLoaded', function() { return; } + // Prüfe auf ErrorLock: Wenn aktiviert und currentCharIndex nicht am Ende des korrekten Präfixes, blockiere Eingabe + if (errorLock && currentCharIndex !== getCorrectPrefixLength()) { + event.preventDefault(); + return; + } + // Behandle normale Zeichen event.preventDefault(); if (currentCharIndex < fullText.length) { @@ -189,8 +413,8 @@ document.addEventListener('DOMContentLoaded', function() { } currentCharIndex++; keyStrokeCount++; - if (!startTime) { - startTime = new Date(); + if (!lastStartTime && !isPaused) { + lastStartTime = new Date(); } updateTextDisplay(); updateStatistics(); @@ -274,12 +498,17 @@ document.addEventListener('DOMContentLoaded', function() { fullText = data.text; currentCharIndex = 0; userInput = ''; - startTime = null; + lastStartTime = null; + totalElapsedTime = 0; + isPaused = false; keyStrokeCount = 0; // Zurücksetzen des Tastaturanschlagszählers lines = fullText.split('\n'); userScrolled = false; // Reset Scroll-Flag updateTextDisplay(); document.getElementById('lesson-title').textContent = data.title; + // Setze Play/Pause-Toggle zurück auf Pause + document.getElementById('play-pause-pause').style.display = 'block'; + document.getElementById('play-pause-play').style.display = 'none'; updateStatistics(); // Aktualisiere die Markierung der aktuellen Lektion updateLessonHighlight(lessonIndex); @@ -290,64 +519,84 @@ document.addEventListener('DOMContentLoaded', function() { }); // Nächster Text Button - nextBtn.addEventListener('click', function() { - // Stoppe Timer und setze Zeit zurück - if (timerInterval) { - clearInterval(timerInterval); - timerInterval = null; - } - startTime = null; - - fetch('/next_text', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + if (nextBtn) { + nextBtn.addEventListener('click', function() { + // Stoppe Timer und setze Zeit zurück + if (timerInterval) { + clearInterval(timerInterval); + timerInterval = null; } - }) - .then(response => response.json()) - .then(data => { - fullText = data.text; - currentCharIndex = 0; - userInput = ''; - startTime = null; - keyStrokeCount = 0; // Zurücksetzen des Tastaturanschlagszählers - lines = fullText.split('\n'); - userScrolled = false; // Reset Scroll-Flag - updateTextDisplay(); - // Aktualisiere den Lektionstitel - document.getElementById('lesson-title').textContent = data.title; - updateStatistics(); - // Aktualisiere die Markierung der aktuellen Lektion - updateLessonHighlight(data.index); - }) - .catch(error => console.error('Fehler:', error)); - }); + + fetch('/next_text', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + alert('Fehler: ' + data.error); + return; + } + fullText = data.text; + currentCharIndex = 0; + userInput = ''; + lastStartTime = null; + totalElapsedTime = 0; + isPaused = false; + keyStrokeCount = 0; // Zurücksetzen des Tastaturanschlagszählers + lines = fullText.split('\n'); + userScrolled = false; // Reset Scroll-Flag + updateTextDisplay(); + // Aktualisiere den Lektionstitel + document.getElementById('lesson-title').textContent = data.title; + // Setze Play/Pause-Toggle zurück auf Pause + document.getElementById('play-pause-pause').style.display = 'block'; + document.getElementById('play-pause-play').style.display = 'none'; + updateStatistics(); + // Aktualisiere die Markierung der aktuellen Lektion + updateLessonHighlight(data.index); + }) + .catch(error => { + console.error('Fehler:', error); + alert('Fehler beim Laden des nächsten Textes: ' + error.message); + }); + }); + } // Beenden Button - endBtn.addEventListener('click', function() { - // Hier könnten wir die aktuelle Statistik speichern - fetch('/end_session', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - } - }) - .then(response => response.json()) - .then(data => { - alert('Sitzung beendet. Fortschritt gespeichert.'); - window.close(); - }) - .catch(error => console.error('Fehler:', error)); - }); + if (endBtn) { + endBtn.addEventListener('click', function() { + // Hier könnten wir die aktuelle Statistik speichern + fetch('/end_session', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + alert('Sitzung beendet. Fortschritt gespeichert.'); + // window.close(); // Funktioniert nicht in allen Browsern + // Stattdessen: Zur Startseite navigieren oder Seite neu laden + window.location.href = '/'; + }) + .catch(error => { + console.error('Fehler:', error); + alert('Fehler beim Beenden der Sitzung: ' + error.message); + }); + }); + } // Statistik aktualisieren function updateStatistics() { - // Berechne verstrichene Zeit nur wenn der Benutzer tippt - let elapsedTime = 0; - if (startTime) { - const now = new Date(); - elapsedTime = (now - startTime) / 1000; // in Sekunden + // Berechne verstrichene Zeit + let elapsedTime = totalElapsedTime; + if (lastStartTime !== null) { + elapsedTime += (new Date() - lastStartTime); } + elapsedTime = elapsedTime / 1000; // in Sekunden // Berechne Statistiken let totalKeyStrokes = keyStrokeCount; // Verwende jetzt die Anzahl aller Tastaturanschläge (inkl. Backspace) @@ -535,4 +784,19 @@ document.addEventListener('DOMContentLoaded', function() { // Initialisiere Toggle-Funktionalität initializeProgressStatsToggle(); + + // Setze Fokus auf den Body für Tastatureingaben + document.body.focus(); +}); + +// Fehlerbehandlung für globale Ereignisse +window.addEventListener('error', function(e) { + console.error('JavaScript Fehler:', e.error); +}); + +// Sicherstellen, dass der Body fokussiert ist +document.addEventListener('click', function() { + if (document.activeElement !== document.body) { + document.body.focus(); + } }); diff --git a/templates/index.html b/templates/index.html index a84b02f..a536f04 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,6 +6,7 @@ Typewriter Trainer +
@@ -15,9 +16,30 @@

Typewriter Trainer

- +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
@@ -47,6 +69,10 @@