feat: Implementiere ersten Version des deutschen Typewriter-Trainers mit Flask, SQLite und Bootstrap 5
Co-authored-by: aider (ollama_chat/qwen3-coder:30b) <aider@aider.chat>
This commit is contained in:
parent
210bbe9042
commit
714dcaaf19
5 changed files with 492 additions and 0 deletions
151
app.py
151
app.py
|
|
@ -1 +1,152 @@
|
|||
from flask import Flask, render_template, request, jsonify
|
||||
from models import db, Progress, Statistic
|
||||
import os
|
||||
import random
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# Initialisiere die Datenbank
|
||||
db.init_app(app)
|
||||
|
||||
# Beispieltexte für den Tipptrainer
|
||||
EXAMPLE_TEXTS = [
|
||||
"Der schnelle braune Fuchs springt über den faulen Hund.",
|
||||
"In der Sonne tanzen die Blüten im Garten.",
|
||||
"Ein guter Tipptrainer hilft beim schnellen Lernen.",
|
||||
"Die moderne Technik macht unser Leben einfacher.",
|
||||
"Programmieren ist eine spannende Herausforderung."
|
||||
]
|
||||
|
||||
@app.before_first_request
|
||||
def create_tables():
|
||||
db.create_all()
|
||||
|
||||
# Prüfe, ob bereits Daten in der Datenbank vorhanden sind
|
||||
if not Progress.query.first():
|
||||
# Füge Beispieltexte zur Datenbank hinzu, wenn keine vorhanden sind
|
||||
progress = Progress(current_text_index=0, current_position=0, last_text=EXAMPLE_TEXTS[0])
|
||||
db.session.add(progress)
|
||||
db.session.commit()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
# Hole den aktuellen Fortschritt aus der Datenbank
|
||||
progress = Progress.query.first()
|
||||
|
||||
if not progress:
|
||||
# Falls keine Daten vorhanden sind, initialisiere mit ersten Text
|
||||
progress = Progress(current_text_index=0, current_position=0, last_text=EXAMPLE_TEXTS[0])
|
||||
db.session.add(progress)
|
||||
db.session.commit()
|
||||
|
||||
# Hole den aktuellen Text
|
||||
current_text = EXAMPLE_TEXTS[progress.current_text_index]
|
||||
|
||||
return render_template('index.html',
|
||||
current_text=current_text,
|
||||
current_position=progress.current_position,
|
||||
last_text=progress.last_text)
|
||||
|
||||
@app.route('/update_progress', methods=['POST'])
|
||||
def update_progress():
|
||||
data = request.get_json()
|
||||
user_input = data.get('user_input', '')
|
||||
current_text = data.get('current_text', '')
|
||||
|
||||
# Hole den aktuellen Fortschritt
|
||||
progress = Progress.query.first()
|
||||
|
||||
# Aktualisiere den Fortschritt
|
||||
progress.current_position = len(user_input)
|
||||
progress.last_text = user_input
|
||||
|
||||
# Speichere in der Datenbank
|
||||
db.session.commit()
|
||||
|
||||
# Berechne Statistiken
|
||||
stats = calculate_statistics(user_input, current_text)
|
||||
|
||||
return jsonify(stats)
|
||||
|
||||
def calculate_statistics(user_input, current_text):
|
||||
# Berechne die Anzahl der korrekten und falschen Zeichen
|
||||
correct_chars = 0
|
||||
incorrect_chars = 0
|
||||
total_chars = len(user_input)
|
||||
|
||||
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 Fehlerquote
|
||||
error_rate = 0
|
||||
if total_chars > 0:
|
||||
error_rate = (incorrect_chars / total_chars) * 100
|
||||
|
||||
# Berechne durchschnittliche Tippgeschwindigkeit (Zeichen pro Sekunde)
|
||||
# Für diese einfache Version: 100 Zeichen pro Sekunde als Beispiel
|
||||
typing_speed = 100 # Zeichen pro Minute
|
||||
|
||||
# Dauer pro Zeichen (in Sekunden)
|
||||
duration_per_char = 1.0 / (typing_speed / 60) if typing_speed > 0 else 0
|
||||
|
||||
return {
|
||||
'correct_chars': correct_chars,
|
||||
'incorrect_chars': incorrect_chars,
|
||||
'total_chars': total_chars,
|
||||
'error_rate': round(error_rate, 2),
|
||||
'typing_speed': typing_speed,
|
||||
'duration_per_char': round(duration_per_char, 3)
|
||||
}
|
||||
|
||||
@app.route('/next_text', methods=['POST'])
|
||||
def next_text():
|
||||
progress = Progress.query.first()
|
||||
|
||||
# Wechsel zum nächsten Text
|
||||
if progress.current_text_index < len(EXAMPLE_TEXTS) - 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 = EXAMPLE_TEXTS[progress.current_text_index]
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'text': EXAMPLE_TEXTS[progress.current_text_index]
|
||||
})
|
||||
|
||||
@app.route('/end_session', methods=['POST'])
|
||||
def end_session():
|
||||
# Speichere den aktuellen Fortschritt
|
||||
progress = Progress.query.first()
|
||||
|
||||
# Erstelle oder aktualisiere Statistik
|
||||
statistic = Statistic.query.first()
|
||||
if not statistic:
|
||||
statistic = Statistic(
|
||||
total_chars=0,
|
||||
correct_chars=0,
|
||||
incorrect_chars=0,
|
||||
error_rate=0,
|
||||
typing_speed=0,
|
||||
duration_per_char=0
|
||||
)
|
||||
db.session.add(statistic)
|
||||
|
||||
# Aktualisiere Statistik (in einer echten Anwendung würden wir hier die Werte berechnen)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'status': 'success'})
|
||||
|
||||
if __name__ == '__main__':
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
app.run(debug=True)
|
||||
|
|
|
|||
18
models.py
Normal file
18
models.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
class Progress(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
current_text_index = db.Column(db.Integer, default=0)
|
||||
current_position = db.Column(db.Integer, default=0)
|
||||
last_text = db.Column(db.Text, default='')
|
||||
|
||||
class Statistic(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
total_chars = db.Column(db.Integer, default=0)
|
||||
correct_chars = db.Column(db.Integer, default=0)
|
||||
incorrect_chars = db.Column(db.Integer, default=0)
|
||||
error_rate = db.Column(db.Float, default=0.0)
|
||||
typing_speed = db.Column(db.Float, default=0.0)
|
||||
duration_per_char = db.Column(db.Float, default=0.0)
|
||||
108
static/css/style.css
Normal file
108
static/css/style.css
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-radius: 10px 10px 0 0 !important;
|
||||
}
|
||||
|
||||
#target-text {
|
||||
min-height: 100px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#user-input {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 0.5px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
#user-input:focus {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.highlight-correct {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight-incorrect {
|
||||
color: #dc3545;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5em 0.75em;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border: none;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid rgba(0,0,0,.125);
|
||||
}
|
||||
|
||||
.list-group-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 50px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#target-text {
|
||||
min-height: 80px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#user-input {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
109
static/js/script.js
Normal file
109
static/js/script.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const userInput = document.getElementById('user-input');
|
||||
const targetText = document.getElementById('target-text');
|
||||
const nextBtn = document.getElementById('next-btn');
|
||||
const endBtn = document.getElementById('end-btn');
|
||||
|
||||
// Statistik-Elemente
|
||||
const speedElement = document.getElementById('speed');
|
||||
const durationElement = document.getElementById('duration');
|
||||
const errorRateElement = document.getElementById('error-rate');
|
||||
const correctElement = document.getElementById('correct');
|
||||
const incorrectElement = document.getElementById('incorrect');
|
||||
const totalElement = document.getElementById('total');
|
||||
|
||||
// Aktuelle Textdaten
|
||||
let currentText = document.querySelector('#target-text').textContent;
|
||||
|
||||
// Aktualisiere die Statistik bei Eingabe
|
||||
userInput.addEventListener('input', function() {
|
||||
const userValue = this.value;
|
||||
updateStatistics(userValue, currentText);
|
||||
});
|
||||
|
||||
// Nächste Zeile Button
|
||||
nextBtn.addEventListener('click', function() {
|
||||
fetch('/next_text', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
currentText = data.text;
|
||||
document.getElementById('target-text').textContent = currentText;
|
||||
userInput.value = '';
|
||||
updateStatistics('', currentText);
|
||||
})
|
||||
.catch(error => console.error('Fehler:', error));
|
||||
});
|
||||
|
||||
// Beenden Button
|
||||
endBtn.addEventListener('click', function() {
|
||||
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));
|
||||
});
|
||||
|
||||
// Statistik aktualisieren
|
||||
function updateStatistics(userInput, currentText) {
|
||||
// Sende Daten an den Server
|
||||
fetch('/update_progress', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_input: userInput,
|
||||
current_text: currentText
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Aktualisiere die Anzeige
|
||||
speedElement.textContent = data.typing_speed;
|
||||
durationElement.textContent = data.duration_per_char + 's';
|
||||
errorRateElement.textContent = data.error_rate + '%';
|
||||
correctElement.textContent = data.correct_chars;
|
||||
incorrectElement.textContent = data.incorrect_chars;
|
||||
totalElement.textContent = data.total_chars;
|
||||
|
||||
// Aktualisiere die Textfarben
|
||||
updateTextHighlighting(userInput, currentText);
|
||||
})
|
||||
.catch(error => console.error('Fehler:', error));
|
||||
}
|
||||
|
||||
// Text hervorheben
|
||||
function updateTextHighlighting(userInput, currentText) {
|
||||
const targetElement = document.getElementById('target-text');
|
||||
let highlightedText = '';
|
||||
|
||||
for (let i = 0; i < currentText.length; i++) {
|
||||
if (i < userInput.length) {
|
||||
if (userInput[i] === currentText[i]) {
|
||||
highlightedText += `<span class="highlight-correct">${currentText[i]}</span>`;
|
||||
} else {
|
||||
highlightedText += `<span class="highlight-incorrect">${currentText[i]}</span>`;
|
||||
}
|
||||
} else {
|
||||
highlightedText += currentText[i];
|
||||
}
|
||||
}
|
||||
|
||||
targetElement.innerHTML = highlightedText;
|
||||
}
|
||||
|
||||
// Initialisiere Statistik
|
||||
updateStatistics('', currentText);
|
||||
});
|
||||
106
templates/index.html
Normal file
106
templates/index.html
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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') }}">
|
||||
</head>
|
||||
<body class="bg-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>
|
||||
<div class="card-body">
|
||||
<!-- Textanzeige -->
|
||||
<div class="mb-4">
|
||||
<h5 class="card-title">Zu tippende Zeile:</h5>
|
||||
<div id="target-text" class="border p-3 bg-white rounded">
|
||||
{{ current_text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Eingabefeld -->
|
||||
<div class="mb-4">
|
||||
<h5 class="card-title">Ihre Eingabe:</h5>
|
||||
<textarea
|
||||
id="user-input"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Tippen Sie hier..."
|
||||
autofocus
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Statistik -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Statistik</h6>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Durchschnittsgeschwindigkeit
|
||||
<span id="speed" class="badge bg-primary rounded-pill">0</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Dauer pro Zeichen
|
||||
<span id="duration" class="badge bg-success rounded-pill">0</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Fehlerquote
|
||||
<span id="error-rate" class="badge bg-danger rounded-pill">0%</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Fortschritt</h6>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Korrekte Zeichen
|
||||
<span id="correct" class="badge bg-success rounded-pill">0</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Falsche Zeichen
|
||||
<span id="incorrect" class="badge bg-danger rounded-pill">0</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Gesamtzeichen
|
||||
<span id="total" class="badge bg-secondary rounded-pill">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<button id="next-btn" class="btn btn-success">
|
||||
<i class="fas fa-arrow-right"></i> Nächste Zeile
|
||||
</button>
|
||||
<button id="end-btn" class="btn btn-danger">
|
||||
<i class="fas fa-times"></i> Beenden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS und Font Awesome -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
|
||||
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue