Metronom läuft, Symbole installiert ...

This commit is contained in:
jamulix 2025-10-25 03:46:47 +02:00 committed by jamulix (aider)
commit 9ef38475ac
4 changed files with 414 additions and 86 deletions

32
app.py
View file

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

View file

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

View file

@ -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 = '<i class="fas fa-sun"></i>';
button.classList.remove('btn-outline-light');
button.classList.add('btn-outline-warning');
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
} else {
button.innerHTML = '<i class="fas fa-cog"></i>';
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();
}
});

View file

@ -6,6 +6,7 @@
<title>Typewriter Trainer</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
</head>
<body class="bg-light" data-bs-theme="light">
<div class="container py-4">
@ -15,9 +16,30 @@
<div class="card-header bg-primary text-white">
<div class="d-flex justify-content-between align-items-center">
<h2 class="mb-0">Typewriter Trainer</h2>
<button id="theme-toggle" class="btn btn-outline-light rounded-circle" style="width: 32px; height: 32px; padding: 0;">
<i class="fas fa-cog"></i>
</button>
<div class="d-flex flex-row-reverse align-items-center gap-2">
<i data-lucide="settings" class="icon-placeholder" style="width: 24px; height: 24px; cursor: pointer;"></i>
<div id="theme-toggle" style="width: 24px; height: 24px; cursor: pointer; position: relative;">
<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="lock-toggle" style="width: 24px; height: 24px; cursor: pointer; position: relative;">
<i data-lucide="lock" id="lock-closed" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0;"></i>
<i data-lucide="unlock" id="lock-open" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0; display: none;"></i>
</div>
<div id="volume-toggle" style="width: 24px; height: 24px; cursor: pointer; position: relative;">
<i data-lucide="volume-2" id="volume-on" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0;"></i>
<i data-lucide="volume-x" id="volume-off" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0; display: none;"></i>
</div>
<div id="play-pause-toggle" style="width: 24px; height: 24px; cursor: pointer; position: relative;">
<i data-lucide="pause" id="play-pause-pause" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0;"></i>
<i data-lucide="play" id="play-pause-play" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0; display: none;"></i>
</div>
<div id="metronome-toggle" style="width: 24px; height: 24px; cursor: pointer; position: relative;">
<i data-lucide="metronome" id="metronome-on" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0; display: none;"></i>
<i data-lucide="circle" id="metronome-off" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0;"></i>
</div>
<i data-lucide="help-circle" class="icon-placeholder" style="width: 24px; height: 24px; cursor: pointer;"></i>
</div>
</div>
</div>
<div class="card-body">
@ -47,6 +69,10 @@
<script>
window.fullText = {{ current_text|tojson }};
window.currentCharIndex = 0;
// Metronom-Einstellungen von Flask übernehmen
window.metronomeEnabled = {{ metronome_enabled|tojson }};
window.metronomeBPM = {{ metronome_bpm|tojson }};
window.metronomeSound = {{ metronome_sound|tojson }};
</script>