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:
parent
9b0b3e7482
commit
54bf091462
4 changed files with 232 additions and 40 deletions
21
app.py
21
app.py
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue