feat(ui): add dark theme and improve lesson navigation

- Add dark/light theme toggle with persistent localStorage
- Implement progress bar showing current lesson position
- Improve lesson highlighting with visual indicators
- Fix WPM calculation to use correct characters only
- Round typing speed to one decimal place for consistency
- Update lesson titles to use 'title' field instead of 'description'
- Make app accessible on network with host '0.0.0.0'
This commit is contained in:
jamulix 2025-10-23 14:21:22 +02:00 committed by jamulix (aider)
commit 54bf091462
4 changed files with 232 additions and 40 deletions

21
app.py
View file

@ -197,14 +197,15 @@ def index():
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('description', f"Lektion {progress.current_text_index + 1}")
current_lesson_title = lesson.get('title', f"Lektion {progress.current_text_index + 1}")
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_title=current_lesson_title,
current_lesson_index=progress.current_text_index)
except Exception as e:
print(f"Fehler in index(): {e}")
@ -362,10 +363,10 @@ def calculate_statistics(user_input, current_text, elapsed_time):
# Berechne Metriken basierend auf echter Zeit
if total_chars > 0 and elapsed_time > 0:
# Zeichen pro Sekunde
# Zeichen pro Sekunde (auf eine Nachkommastelle gerundet)
chars_per_second = total_chars / elapsed_time
# Wörter pro Minute (Annahme: 1 Wort = 5 Zeichen)
words_per_minute = (total_chars / 5) / (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
else:
@ -382,9 +383,9 @@ def calculate_statistics(user_input, current_text, elapsed_time):
'incorrect_chars': incorrect_chars,
'total_chars': total_chars,
'error_rate': round(error_rate, 1),
'typing_speed': round(chars_per_second, 2),
'typing_speed': round(chars_per_second, 1), # Auf eine Nachkommastelle
'duration_per_char': round(duration_per_char, 3),
'words_per_minute': round(words_per_minute, 2),
'words_per_minute': round(words_per_minute, 2), # Auf zwei Nachkommastellen
'accuracy': round(accuracy, 2)
}
@ -414,7 +415,7 @@ def next_text():
# Bestimme den Titel der neuen Lektion
new_lesson = lessons[progress.current_text_index]
lesson_title = new_lesson.get('description', f"Lektion {progress.current_text_index + 1}")
lesson_title = new_lesson.get('title', f"Lektion {progress.current_text_index + 1}")
return jsonify({
'text': new_lesson['text'],
@ -452,7 +453,7 @@ def set_lesson():
# Bestimme den Titel der neuen Lektion
new_lesson = lessons[lesson_index]
lesson_title = new_lesson.get('description', f"Lektion {lesson_index + 1}")
lesson_title = new_lesson.get('title', f"Lektion {lesson_index + 1}")
return jsonify({
'text': new_lesson['text'],
@ -480,4 +481,4 @@ if __name__ == '__main__':
print(f"Debug-Info verfügbar unter: http://127.0.0.1:5000/debug")
print("=" * 50)
app.run(debug=True)
app.run(debug=True, host='0.0.0.0', port=5000)

View file

@ -1,6 +1,104 @@
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: background-color 0.3s, color 0.3s;
}
/* Dark Theme Styles */
[data-bs-theme="dark"] {
--bs-body-bg: #121212;
--bs-body-color: #e0e0e0;
--bs-light: #1e1e1e;
--bs-dark: #f8f9fa;
}
[data-bs-theme="dark"] body {
background-color: #121212;
color: #e0e0e0;
}
[data-bs-theme="dark"] .bg-light {
background-color: #1e1e1e !important;
}
[data-bs-theme="dark"] .card {
background-color: #1e1e1e;
color: #e0e0e0;
border-color: #333;
}
[data-bs-theme="dark"] .card-header {
background-color: #0d6efd !important;
color: white;
}
[data-bs-theme="dark"] .list-group-item {
background-color: #1e1e1e;
color: #e0e0e0;
border-color: #333;
}
/* Light Mode für Text- und Eingabefelder */
#target-text,
#user-input {
background-color: white;
color: #000;
border-color: #dee2e6;
}
/* Dark Mode für Text- und Eingabefelder */
[data-bs-theme="dark"] #target-text,
[data-bs-theme="dark"] #user-input {
background-color: #2d2d2d;
color: #e0e0e0;
border-color: #444;
}
/* Verbesserte Lesbarkeit für Hervorhebungen im Dark-Mode */
[data-bs-theme="dark"] .highlight-correct {
color: #4ade80; /* Helleres Grün für besseren Kontrast */
font-weight: bold;
}
[data-bs-theme="dark"] .highlight-incorrect {
color: #f87171; /* Helleres Rot für besseren Kontrast */
}
[data-bs-theme="dark"] .highlight-space,
[data-bs-theme="dark"] .highlight-tab,
[data-bs-theme="dark"] .highlight-newline {
color: #a0a0a0; /* Hellgrau für Sonderzeichen */
}
[data-bs-theme="dark"] .form-control {
background-color: #2d2d2d;
color: #e0e0e0;
border-color: #444;
}
[data-bs-theme="dark"] .form-control:focus {
background-color: #2d2d2d;
color: #e0e0e0;
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
[data-bs-theme="dark"] .lesson-link:hover {
background-color: #333;
}
[data-bs-theme="dark"] .progress {
background-color: #2d2d2d;
}
[data-bs-theme="dark"] .btn-outline-secondary {
color: #e0e0e0;
border-color: #6c757d;
}
[data-bs-theme="dark"] .btn-outline-secondary:hover {
background-color: #6c757d;
color: #fff;
}
.card {
@ -113,3 +211,41 @@ body {
font-size: 1rem;
}
}
/* Theme-Toggle-Button */
#theme-toggle {
display: flex;
align-items: center;
justify-content: center;
}
/* Zusätzliche Stile für die aktive Lektion */
.lesson-link:hover {
background-color: #e9ecef;
border-radius: 3px;
}
/* Stile für Lektionsnummern */
.lesson-link {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
/* Helle Hintergrundfarbe für den Success-Button */
.btn-success {
background-color: #90EE90 !important;
border-color: #90EE90 !important;
color: #006400 !important;
}
[data-bs-theme="dark"] .btn-success {
background-color: #90EE90 !important;
border-color: #90EE90 !important;
color: #006400 !important;
}
.bg-success {
background-color: #90EE90 !important;
color: #006400 !important;
}

View file

@ -1,4 +1,36 @@
// Theme Management
function initializeTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.body.setAttribute('data-bs-theme', savedTheme);
updateThemeButton(savedTheme);
}
function toggleTheme() {
const currentTheme = document.body.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.body.setAttribute('data-bs-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeButton(newTheme);
}
function updateThemeButton(theme) {
const button = document.getElementById('theme-toggle');
if (theme === 'dark') {
button.innerHTML = '<i class="fas fa-sun"></i>';
button.classList.remove('btn-outline-light');
button.classList.add('btn-outline-warning');
} else {
button.innerHTML = '<i class="fas fa-cog"></i>';
button.classList.remove('btn-outline-warning');
button.classList.add('btn-outline-light');
}
}
document.addEventListener('DOMContentLoaded', function() {
// Theme initialisieren
initializeTheme();
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
const userInput = document.getElementById('user-input');
const targetText = document.getElementById('target-text');
const nextBtn = document.getElementById('next-btn');
@ -28,14 +60,11 @@ document.addEventListener('DOMContentLoaded', function() {
userInput.addEventListener('keydown', function(event) {
if (!startTime && event.key.length === 1) { // Nur Buchstaben, Zahlen, etc.
startTime = new Date();
// Starte Timer für Echtzeit-Update
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(updateTimer, 100);
}
});
// Aktualisiere die Statistik bei Eingabe
userInput.addEventListener('input', function() {
// Aktualisiere die Statistik bei jedem Tastendruck
userInput.addEventListener('keyup', function() {
const userValue = this.value;
const currentLine = lines[currentLineIndex];
updateStatistics(userValue, currentLine);
@ -49,11 +78,6 @@ document.addEventListener('DOMContentLoaded', function() {
updateStatistics('', lines[currentLineIndex]);
} else {
// Lektion beendet - Statistik speichern
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
// Hier könnten wir die Statistik an den Server senden
alert('Lektion beendet!');
// Setze zurück für nächste Lektion?
// Oder automatisch zur nächsten Lektion?
@ -61,15 +85,20 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Timer für Echtzeit-Update der Statistik
function updateTimer() {
if (startTime) {
const userValue = userInput.value;
const currentLine = lines[currentLineIndex];
updateStatistics(userValue, currentLine);
}
// Funktion zum Aktualisieren der Lektionsmarkierung
function updateLessonHighlight(lessonIndex) {
document.querySelectorAll('.lesson-link').forEach(link => {
const index = parseInt(link.getAttribute('data-lesson-index'));
if (index === lessonIndex) {
link.classList.add('fw-bold', 'text-danger');
link.classList.remove('text-primary');
} else {
link.classList.remove('fw-bold', 'text-danger');
link.classList.add('text-primary');
}
});
}
// Event-Listener für Lektions-Links
document.querySelectorAll('.lesson-link').forEach(link => {
link.addEventListener('click', function() {
@ -103,6 +132,8 @@ document.addEventListener('DOMContentLoaded', function() {
userInput.value = '';
userInput.focus();
updateStatistics('', lines[currentLineIndex]);
// Aktualisiere die Markierung der aktuellen Lektion
updateLessonHighlight(lessonIndex);
}
})
.catch(error => console.error('Fehler:', error));
@ -135,6 +166,8 @@ document.addEventListener('DOMContentLoaded', function() {
userInput.value = '';
userInput.focus(); // Fokus zurück auf Eingabefeld
updateStatistics('', lines[currentLineIndex]);
// Aktualisiere die Markierung der aktuellen Lektion
updateLessonHighlight(data.index);
})
.catch(error => console.error('Fehler:', error));
});
@ -158,14 +191,14 @@ document.addEventListener('DOMContentLoaded', function() {
// Statistik aktualisieren
function updateStatistics(userInput, currentText) {
// Berechne verstrichene Zeit
// Berechne verstrichene Zeit nur wenn der Benutzer tippt
let elapsedTime = 0;
if (startTime) {
const now = new Date();
elapsedTime = (now - startTime) / 1000; // in Sekunden
}
// Sende Daten an den Server
// Sende Daten an den Server nur wenn der Benutzer tippt oder klickt
fetch('/update_progress', {
method: 'POST',
headers: {

View file

@ -7,19 +7,40 @@
<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') }}">
</head>
<body class="bg-light">
<body class="bg-light" data-bs-theme="light">
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white text-center">
<h2>Typewriter Trainer</h2>
<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>
</div>
<div class="card-body">
<!-- Fortschrittsbalken -->
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<small>Fortschritt: {{ current_lesson_index + 1 }} von {{ lessons|length }}</small>
<small>{{ ((current_lesson_index + 1) / lessons|length * 100) | round | int }}%</small>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar" role="progressbar"
style="width: {{ ((current_lesson_index + 1) / lessons|length * 100) | round | int }}%;"
aria-valuenow="{{ current_lesson_index + 1 }}"
aria-valuemin="0"
aria-valuemax="{{ lessons|length }}">
</div>
</div>
</div>
<!-- Textanzeige -->
<div class="mb-4">
<h5 class="card-title">Vorlage: <span id="lesson-title">{{ current_lesson_title }}</span></h5>
<div id="target-text" class="border p-3 bg-white rounded" style="white-space: pre-wrap; font-family: monospace;">
<div id="target-text" class="border p-3 rounded" style="white-space: pre-wrap; font-family: monospace;">
<!-- Text wird zeilenweise durch JavaScript eingefügt -->
</div>
</div>
@ -92,13 +113,14 @@
<!-- Lektionen -->
<div class="mb-4">
<h5 class="card-title">Lektionen</h5>
<div class="row">
<div class="d-flex flex-wrap justify-content-start" id="lessons-container">
{% for lesson in lessons %}
<div class="col-md-1 col-2 mb-1 text-center">
<span class="lesson-link text-primary" style="cursor: pointer; font-size: 0.9rem;"
data-lesson-index="{{ loop.index0 }}">
{{ loop.index0 + 1 }}
</span>
<div class="mb-1 me-1" style="width: 40px;">
<div class="lesson-link {% if loop.index0 == current_lesson_index %}fw-bold text-danger{% else %}text-primary{% endif %}"
style="cursor: pointer; font-size: 0.9rem; text-align: center; white-space: nowrap; min-height: 24px; display: flex; align-items: center; justify-content: center;"
data-lesson-index="{{ loop.index0 }}">
{{ lesson.lesson if lesson.lesson is defined else loop.index0 + 1 }}
</div>
</div>
{% endfor %}
</div>