Release v0.3.0: UI-Verbesserungen und Metronom-Optimierung
Features: - Maximale Metronom-Geschwindigkeit konfigurierbar (Standard: 100 BPM) - Metronom läuft präzise unabhängig von Tastatureingaben - Präzisions-Anzeige statt Fehlerquote (positiver formuliert) - Versionsnummer im Header anzeigen UI-Verbesserungen: - Dezente Statistik-Anzeige ohne bunte Badges - Einheitliches Button-Design (Blau: Weiter/Leistung/Statistik, Grün: Weiter) - Rechtsbündige Werte mit konsistentem Abstand - Weiße Beschriftung bei primären Buttons Technisch: - Settings werden persistent in Datenbank gespeichert - Alembic-Migration für max_bpm_speed - Audio-Timing mit Web Audio API statt setTimeout - Metronom-Code aufgeräumt und optimiert 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3cacbe7544
commit
94b6dd91f0
11 changed files with 204 additions and 114 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
v0.2.0
|
||||
v0.3.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
"""Add max_bpm_speed to UserSettings
|
||||
|
||||
Revision ID: 5fcbe91de50b
|
||||
Revises: 8f83d622eb4b
|
||||
Create Date: 2025-10-28 11:34:47.704389
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '5fcbe91de50b'
|
||||
down_revision: Union[str, Sequence[str], None] = '8f83d622eb4b'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('user_settings', sa.Column('max_bpm_speed', sa.Integer(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('user_settings', 'max_bpm_speed')
|
||||
# ### end Alembic commands ###
|
||||
45
app.py
45
app.py
|
|
@ -228,6 +228,7 @@ def save_user_settings(
|
|||
metronome_mode: str,
|
||||
metronome_speed: int,
|
||||
target_error_rate: int,
|
||||
max_bpm_speed: int,
|
||||
speed_display: str
|
||||
) -> None:
|
||||
"""Speichert Benutzereinstellungen in der Datenbank"""
|
||||
|
|
@ -239,6 +240,7 @@ def save_user_settings(
|
|||
settings.metronome_mode = metronome_mode
|
||||
settings.metronome_speed = metronome_speed
|
||||
settings.target_error_rate = target_error_rate
|
||||
settings.max_bpm_speed = max_bpm_speed
|
||||
settings.speed_display = speed_display
|
||||
db.session.commit()
|
||||
logger.info("Benutzereinstellungen gespeichert")
|
||||
|
|
@ -399,6 +401,7 @@ def index():
|
|||
metronome_mode = user_settings.metronome_mode
|
||||
metronome_speed = user_settings.metronome_speed
|
||||
target_error_rate = user_settings.target_error_rate
|
||||
max_bpm_speed = getattr(user_settings, 'max_bpm_speed', 100) # Fallback für Kompatibilität
|
||||
speed_display = user_settings.speed_display
|
||||
|
||||
# Lade die gespeicherte Benutzereingabe und Cursor-Position aus der Datenbank
|
||||
|
|
@ -410,7 +413,7 @@ def index():
|
|||
key_stroke_count = session.get('key_stroke_count', 0)
|
||||
is_paused = session.get('is_paused', False)
|
||||
|
||||
return render_template('index.html',
|
||||
return render_template('index.html',
|
||||
current_text=current_text,
|
||||
current_position=cursor_position,
|
||||
last_text=user_input,
|
||||
|
|
@ -426,6 +429,7 @@ def index():
|
|||
metronome_mode=metronome_mode,
|
||||
metronome_speed=metronome_speed,
|
||||
target_error_rate=target_error_rate,
|
||||
max_bpm_speed=max_bpm_speed,
|
||||
speed_display=speed_display)
|
||||
|
||||
|
||||
|
|
@ -950,11 +954,20 @@ def hilfe():
|
|||
@app.route('/settings')
|
||||
def settings():
|
||||
"""Einstellungsseite"""
|
||||
return render_template('settings.html')
|
||||
# Lade Einstellungen aus Datenbank mit Defaults
|
||||
user_settings = get_or_create_user_settings()
|
||||
current_settings = {
|
||||
'metronome_mode': user_settings.metronome_mode,
|
||||
'metronome_speed': user_settings.metronome_speed,
|
||||
'target_error_rate': user_settings.target_error_rate,
|
||||
'max_bpm_speed': getattr(user_settings, 'max_bpm_speed', 100),
|
||||
'speed_display': user_settings.speed_display
|
||||
}
|
||||
return render_template('settings.html', settings=current_settings)
|
||||
|
||||
@app.route('/save_settings', methods=['POST'])
|
||||
def save_settings():
|
||||
"""Speichert die Einstellungen in der Session"""
|
||||
"""Speichert die Einstellungen persistent in der Datenbank"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
|
|
@ -969,6 +982,7 @@ def save_settings():
|
|||
try:
|
||||
metronome_speed = int(data.get('metronome_speed', 60))
|
||||
target_error_rate = int(data.get('target_error_rate', 5))
|
||||
max_bpm_speed = int(data.get('max_bpm_speed', 100))
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'error': 'Ungültige numerische Werte'}), 400
|
||||
|
||||
|
|
@ -979,15 +993,31 @@ def save_settings():
|
|||
if not (0 <= target_error_rate <= 100):
|
||||
return jsonify({'error': 'Fehlerrate muss zwischen 0 und 100 liegen'}), 400
|
||||
|
||||
if not (40 <= max_bpm_speed <= 200):
|
||||
return jsonify({'error': 'Maximale BPM-Geschwindigkeit muss zwischen 40 und 200 liegen'}), 400
|
||||
|
||||
# Validiere speed_display
|
||||
speed_display = data.get('speed_display', 'zpm')
|
||||
if speed_display not in ['zpm', 'wpm']:
|
||||
return jsonify({'error': 'Ungültige Geschwindigkeitsanzeige'}), 400
|
||||
|
||||
session['metronome_mode'] = metronome_mode
|
||||
session['metronome_speed'] = metronome_speed
|
||||
session['target_error_rate'] = target_error_rate
|
||||
session['speed_display'] = speed_display
|
||||
# Speichere in Datenbank
|
||||
progress = Progress.query.first()
|
||||
current_lesson = progress.current_text_index if progress else 0
|
||||
|
||||
save_user_settings(
|
||||
current_lesson_index=current_lesson,
|
||||
metronome_enabled=True, # Metronome bleibt aktiviert
|
||||
metronome_bpm=metronome_speed,
|
||||
metronome_sound='beep',
|
||||
metronome_mode=metronome_mode,
|
||||
metronome_speed=metronome_speed,
|
||||
target_error_rate=target_error_rate,
|
||||
max_bpm_speed=max_bpm_speed,
|
||||
speed_display=speed_display
|
||||
)
|
||||
|
||||
logger.info(f"Einstellungen gespeichert: mode={metronome_mode}, speed={metronome_speed}, max_bpm={max_bpm_speed}")
|
||||
return jsonify({'status': 'success'})
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler in save_settings: {e}", exc_info=True)
|
||||
|
|
@ -1052,6 +1082,7 @@ def end_session():
|
|||
metronome_mode=data.get('metronome_mode', 'automatic'),
|
||||
metronome_speed=data.get('metronome_speed', 60),
|
||||
target_error_rate=data.get('target_error_rate', 5),
|
||||
max_bpm_speed=data.get('max_bpm_speed', 100),
|
||||
speed_display=data.get('speed_display', 'zpm')
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class UserSettings(db.Model):
|
|||
metronome_mode = db.Column(db.String(20), default='automatic')
|
||||
metronome_speed = db.Column(db.Integer, default=60)
|
||||
target_error_rate = db.Column(db.Integer, default=5)
|
||||
max_bpm_speed = db.Column(db.Integer, default=100)
|
||||
speed_display = db.Column(db.String(10), default='zpm')
|
||||
# Zeitstempel
|
||||
updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())
|
||||
|
|
|
|||
|
|
@ -157,11 +157,9 @@ body {
|
|||
.input-toggle {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.progress-toggle:hover,
|
||||
.statistics-link:hover,
|
||||
.input-toggle:hover {
|
||||
color: #ffc107 !important;
|
||||
text-shadow: 0 0 10px #ffc107, 0 0 20px #ffc107;
|
||||
|
|
@ -401,3 +399,25 @@ body {
|
|||
background-color: #90EE90 !important;
|
||||
color: #006400 !important;
|
||||
}
|
||||
|
||||
/* Statistik Button behält weiße Schrift */
|
||||
.statistics-link.btn-primary {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.statistics-link.btn-primary:hover {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Weiter Button in Grün (wie korrekte Zeichen) */
|
||||
#next-btn {
|
||||
background-color: #28a745 !important;
|
||||
border-color: #28a745 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] #next-btn {
|
||||
background-color: #4ade80 !important;
|
||||
border-color: #4ade80 !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,10 @@ class TypewriterApp {
|
|||
this.themeManager.setupEventListeners();
|
||||
|
||||
// Metronome
|
||||
this.adaptiveMetronome = new AdaptiveMetronome();
|
||||
this.adaptiveMetronome = new AdaptiveMetronome({
|
||||
initialSpeed: window.metronomeBPM || 60,
|
||||
maxSpeed: window.maxBpmSpeed || 100
|
||||
});
|
||||
this.metronomePlayer = new MetronomePlayer();
|
||||
this.metronomePlayer.enabled = window.metronomeEnabled || true;
|
||||
this.metronomePlayer.bpm = window.metronomeBPM || 60;
|
||||
|
|
|
|||
|
|
@ -1,78 +1,57 @@
|
|||
/**
|
||||
* Adaptive Metronome Module
|
||||
* Implements an intelligent metronome that adjusts speed based on typing accuracy
|
||||
*/
|
||||
|
||||
// Adaptive Metronome Module
|
||||
export class AdaptiveMetronome {
|
||||
constructor() {
|
||||
this.speed = 60.0; // Starting speed in BPM
|
||||
this.accuracy_history = []; // Last 50 results (1=correct, 0=incorrect)
|
||||
this.consecutive_correct = 0; // Consecutive correct characters
|
||||
this.target_accuracy = 0.96; // Target accuracy (96%)
|
||||
constructor(options = {}) {
|
||||
this.speed = options.initialSpeed || 60.0; // Starting speed in BPM
|
||||
this.max_speed = options.maxSpeed || 100.0; // Maximum speed in BPM (default: 100)
|
||||
this.accuracy_history = []; // Last 50 results (1=correct, 0=incorrect)
|
||||
this.consecutive_correct = 0; // Consecutive correct characters
|
||||
this.target_accuracy = 0.96; // Target accuracy (96%)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a keystroke and adjust speed
|
||||
* @param {boolean} is_correct - Whether the typed character was correct
|
||||
* @param {number} current_difficulty - Difficulty factor (1.0 = normal)
|
||||
* @returns {number} New BPM speed
|
||||
*/
|
||||
// Process a keystroke and adjust speed
|
||||
process_keystroke(is_correct, current_difficulty = 1.0) {
|
||||
// 1. Update history
|
||||
this.accuracy_history.push(is_correct ? 1.0 : 0.0);
|
||||
if (this.accuracy_history.length > 50) {
|
||||
this.accuracy_history.shift();
|
||||
}
|
||||
|
||||
// 2. Count consecutive correct characters
|
||||
if (is_correct) {
|
||||
this.consecutive_correct += 1;
|
||||
} else {
|
||||
this.consecutive_correct = 0;
|
||||
}
|
||||
|
||||
// 3. Adjust speed
|
||||
return this._adjust_speed(current_difficulty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust speed based on current accuracy
|
||||
* @param {number} difficulty - Difficulty factor
|
||||
* @returns {number} New BPM speed
|
||||
* @private
|
||||
*/
|
||||
// Adjust speed based on current accuracy
|
||||
_adjust_speed(difficulty) {
|
||||
if (this.accuracy_history.length === 0) {
|
||||
return this.speed;
|
||||
}
|
||||
|
||||
let current_accuracy = this.accuracy_history.reduce((a, b) => a + b, 0) / this.accuracy_history.length;
|
||||
let adjusted_target = this.target_accuracy - (difficulty * 0.05);
|
||||
|
||||
// Multiple adjustment strategies:
|
||||
if (this.consecutive_correct >= 15) {
|
||||
// Reward for sustained accuracy
|
||||
this.speed *= 1.03;
|
||||
this.speed *= 1.03; // Reward for sustained accuracy
|
||||
} else if (current_accuracy > adjusted_target + 0.04) {
|
||||
// Too easy - accelerate more
|
||||
this.speed *= 1.02;
|
||||
this.speed *= 1.02; // Too easy - accelerate more
|
||||
} else if (current_accuracy < adjusted_target - 0.06) {
|
||||
// Too difficult - brake harder
|
||||
this.speed *= 0.92;
|
||||
this.speed *= 0.92; // Too difficult - brake harder
|
||||
} else if (current_accuracy < adjusted_target) {
|
||||
// Slightly below target - gentle braking
|
||||
this.speed *= 0.97;
|
||||
this.speed *= 0.97; // Slightly below target - gentle braking
|
||||
} else {
|
||||
// In target range - gentle acceleration
|
||||
this.speed *= 1.01;
|
||||
this.speed *= 1.01; // In target range - gentle acceleration
|
||||
}
|
||||
|
||||
// Speed limits
|
||||
this.speed = Math.max(40, Math.min(200, this.speed));
|
||||
// Speed limits (configurable max_speed, absolute min 40, absolute max 200)
|
||||
this.speed = Math.max(40, Math.min(Math.min(200, this.max_speed), this.speed));
|
||||
return this.speed;
|
||||
}
|
||||
}
|
||||
|
||||
// MetronomePlayer: encapsulates the timed metronome logic
|
||||
export class MetronomePlayer {
|
||||
constructor() {
|
||||
this.audioContext = null;
|
||||
|
|
@ -83,35 +62,21 @@ export class MetronomePlayer {
|
|||
this.sound = 'beep';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a beep sound
|
||||
* @param {number} vol - Volume (0-1)
|
||||
* @param {number} freq - Frequency in Hz
|
||||
* @param {number} duration - Duration in ms
|
||||
*/
|
||||
createBeepSound(vol, freq, duration) {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
const oscillator = this.audioContext.createOscillator();
|
||||
const gainNode = this.audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(this.audioContext.destination);
|
||||
|
||||
gainNode.gain.value = vol;
|
||||
oscillator.frequency.value = freq;
|
||||
oscillator.type = 'sine';
|
||||
|
||||
oscillator.start();
|
||||
setTimeout(() => {
|
||||
oscillator.stop();
|
||||
}, duration);
|
||||
oscillator.start(this.audioContext.currentTime);
|
||||
oscillator.stop(this.audioContext.currentTime + duration / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a click sound
|
||||
*/
|
||||
createClickSound() {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
|
@ -119,28 +84,22 @@ export class MetronomePlayer {
|
|||
const bufferSize = this.audioContext.sampleRate * 0.01;
|
||||
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
|
||||
const gainNode = this.audioContext.createGain();
|
||||
gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + 0.1);
|
||||
|
||||
source.connect(gainNode);
|
||||
gainNode.connect(this.audioContext.destination);
|
||||
source.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Play metronome sound and provide visual feedback
|
||||
*/
|
||||
// Play metronome sound and provide visual feedback; called ONLY by scheduler
|
||||
play() {
|
||||
// Visual feedback
|
||||
// Visual feedback (optional)
|
||||
const cursorElement = document.querySelector('.cursor-char');
|
||||
if (cursorElement) {
|
||||
cursorElement.style.animation = 'pulse 0.3s';
|
||||
|
|
@ -148,12 +107,10 @@ export class MetronomePlayer {
|
|||
cursorElement.style.animation = '';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Audio feedback (only if enabled)
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.sound === 'beep') {
|
||||
this.createBeepSound(0.1, 800, 50);
|
||||
} else {
|
||||
|
|
@ -161,22 +118,16 @@ export class MetronomePlayer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the metronome
|
||||
*/
|
||||
// Start the metronome timer sequence
|
||||
start() {
|
||||
this.stop();
|
||||
this.isRunning = true;
|
||||
this.scheduleNextBeat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the next metronome beat
|
||||
* @private
|
||||
*/
|
||||
// Private: schedules next beat - this is the ONLY place play() is called
|
||||
scheduleNextBeat() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
const interval = (60 / this.bpm) * 1000;
|
||||
this.timeout = setTimeout(() => {
|
||||
this.play();
|
||||
|
|
@ -184,9 +135,6 @@ export class MetronomePlayer {
|
|||
}, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the metronome
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
if (this.timeout) {
|
||||
|
|
@ -195,28 +143,34 @@ export class MetronomePlayer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set BPM (takes effect at next beat)
|
||||
* @param {number} bpm - Beats per minute
|
||||
*/
|
||||
// Set BPM - takes effect at next scheduled beat without interrupting current beat
|
||||
setBPM(bpm) {
|
||||
this.bpm = bpm;
|
||||
// No restart needed - change takes effect at next scheduled beat
|
||||
// No restart - allows metronome to run continuously during typing
|
||||
// New BPM will be applied when next beat is scheduled
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle metronome on/off
|
||||
*/
|
||||
// Toggle metronome enabled/disabled
|
||||
toggle() {
|
||||
this.enabled = !this.enabled;
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sound type
|
||||
* @param {string} sound - 'beep' or 'click'
|
||||
*/
|
||||
// Set sound type ('beep' or 'click')
|
||||
setSound(sound) {
|
||||
this.sound = sound;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Integration Example (not actual code) ---
|
||||
* Event Handler does NOT trigger metronome sound directly!
|
||||
*
|
||||
* document.addEventListener('keydown', (event) => {
|
||||
* // Update typing statistics and BPM, but NO direct sound trigger!
|
||||
* let isCorrect = ...; // your logic here
|
||||
* let currentDifficulty = ...; // your logic here
|
||||
* let newBPM = adaptiveMetronome.process_keystroke(isCorrect, currentDifficulty);
|
||||
* metronomePlayer.setBPM(newBPM); // Sound plays on next scheduled beat
|
||||
* });
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export class StatisticsManager {
|
|||
if (speedLabel) speedLabel.textContent = 'ZPM:';
|
||||
}
|
||||
|
||||
if (errorRateElement) errorRateElement.textContent = stats.errorRate.toFixed(1);
|
||||
if (errorRateElement) errorRateElement.textContent = (100.0 - stats.errorRate).toFixed(1);
|
||||
if (correctElement) correctElement.textContent = stats.correctKeyStrokes;
|
||||
if (incorrectElement) incorrectElement.textContent = stats.incorrectKeyStrokes;
|
||||
if (totalElement) totalElement.textContent = stats.totalKeyStrokes;
|
||||
|
|
|
|||
|
|
@ -225,13 +225,13 @@ export class UIManager {
|
|||
|
||||
if (visible) {
|
||||
progressContainer.classList.add('visible');
|
||||
performanceBtn.classList.remove('btn-light-custom');
|
||||
performanceBtn.classList.add('btn-primary');
|
||||
} else {
|
||||
progressContainer.classList.remove('visible');
|
||||
performanceBtn.classList.remove('btn-primary');
|
||||
performanceBtn.classList.add('btn-light-custom');
|
||||
}
|
||||
|
||||
// Button bleibt immer blau (btn-primary)
|
||||
performanceBtn.classList.remove('btn-light-custom');
|
||||
performanceBtn.classList.add('btn-primary');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<div class="card shadow">
|
||||
<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>
|
||||
<h2 class="mb-0">Typewriter Trainer <small class="text-white-50">{{ APP_VERSION }}</small></h2>
|
||||
<div class="d-flex flex-row-reverse align-items-center gap-2">
|
||||
<button id="settings-toggle"
|
||||
style="width: 24px; height: 24px; cursor: pointer; border: none; background: none; padding: 0; color: white;"
|
||||
|
|
@ -98,6 +98,7 @@
|
|||
window.metronomeMode = {{ metronome_mode|tojson }};
|
||||
window.metronomeSpeed = {{ metronome_speed|tojson }};
|
||||
window.targetErrorRate = {{ target_error_rate|tojson }};
|
||||
window.maxBpmSpeed = {{ max_bpm_speed|tojson }};
|
||||
</script>
|
||||
|
||||
|
||||
|
|
@ -132,15 +133,15 @@
|
|||
<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" role="status" aria-live="polite">0</span>
|
||||
<span id="correct" class="text-end fw-semibold" style="min-width: 60px;" role="status" aria-live="polite">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" role="status" aria-live="polite">0</span>
|
||||
<span id="incorrect" class="text-end fw-semibold" style="min-width: 60px;" role="status" aria-live="polite">0</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Tastaturanschläge
|
||||
<span id="total" class="badge bg-secondary rounded-pill" role="status" aria-live="polite">0</span>
|
||||
<span id="total" class="text-end fw-semibold" style="min-width: 60px;" role="status" aria-live="polite">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -153,15 +154,15 @@
|
|||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Zeichen pro Minute (ZPM)
|
||||
<span id="speed" class="badge bg-primary rounded-pill">0</span>
|
||||
<span id="speed" class="text-end fw-semibold" style="min-width: 60px;">0</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Fehler in %
|
||||
<span id="error-rate" class="badge bg-danger rounded-pill">0</span>
|
||||
Präzision in %
|
||||
<span id="error-rate" class="text-end fw-semibold" style="min-width: 60px;">100.0</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Wörter pro Minute (WPM)
|
||||
<span id="wpm" class="badge bg-info rounded-pill">0</span>
|
||||
<span id="wpm" class="text-end fw-semibold" style="min-width: 60px;">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -172,13 +173,13 @@
|
|||
|
||||
<!-- Buttons -->
|
||||
<div class="d-flex justify-content-between flex-wrap gap-2">
|
||||
<button id="next-btn" class="btn btn-success flex-fill">
|
||||
<button id="next-btn" class="btn btn-primary flex-fill">
|
||||
<i class="fas fa-arrow-right"></i> Weiter
|
||||
</button>
|
||||
<button id="performance-btn" class="btn btn-light-custom flex-fill">
|
||||
<button id="performance-btn" class="btn btn-primary flex-fill">
|
||||
<i class="fas fa-chart-line"></i> Leistung
|
||||
</button>
|
||||
<button class="btn btn-light-custom flex-fill statistics-link">
|
||||
<button class="btn btn-primary flex-fill statistics-link">
|
||||
<i class="fas fa-chart-bar"></i> Statistik
|
||||
</button>
|
||||
<button id="end-btn" class="btn btn-danger flex-fill">
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@
|
|||
<label for="targetErrorRate" class="form-label">Ziel-Fehlerrate (%): <span id="errorRateValue">5</span></label>
|
||||
<input type="range" class="form-range" id="targetErrorRate" min="1" max="20" step="1" value="5">
|
||||
<small class="form-text text-muted">Die Metronom-Geschwindigkeit wird automatisch angepasst, um diese Fehlerrate nicht zu überschreiten</small>
|
||||
|
||||
<div class="mt-3">
|
||||
<label for="maxBpmSpeed" class="form-label">Maximale Metronom-Geschwindigkeit (ZPM): <span id="maxBpmValue">100</span></label>
|
||||
<input type="range" class="form-range" id="maxBpmSpeed" min="40" max="200" step="5" value="100">
|
||||
<small class="form-text text-muted">Bei fehlerfreier Eingabe wird diese Geschwindigkeit nicht überschritten (Default: 100 ZPM)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -153,11 +159,17 @@
|
|||
document.getElementById('errorRateValue').textContent = this.value;
|
||||
});
|
||||
|
||||
// Schieberegler für maximale BPM-Geschwindigkeit
|
||||
document.getElementById('maxBpmSpeed').addEventListener('input', function() {
|
||||
document.getElementById('maxBpmValue').textContent = this.value;
|
||||
});
|
||||
|
||||
// Einstellungen speichern
|
||||
document.getElementById('saveSettings').addEventListener('click', function() {
|
||||
const metronomeMode = document.querySelector('input[name="metronomeMode"]:checked').value;
|
||||
const metronomeSpeed = document.getElementById('metronomeSpeed').value;
|
||||
const targetErrorRate = document.getElementById('targetErrorRate').value;
|
||||
const maxBpmSpeed = document.getElementById('maxBpmSpeed').value;
|
||||
const speedDisplay = document.querySelector('input[name="speedDisplay"]:checked').value;
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
|
||||
|
|
@ -171,6 +183,7 @@
|
|||
metronome_mode: metronomeMode,
|
||||
metronome_speed: parseInt(metronomeSpeed),
|
||||
target_error_rate: parseInt(targetErrorRate),
|
||||
max_bpm_speed: parseInt(maxBpmSpeed),
|
||||
speed_display: speedDisplay
|
||||
})
|
||||
})
|
||||
|
|
@ -189,10 +202,45 @@
|
|||
});
|
||||
});
|
||||
|
||||
// Einstellungen aus Backend laden
|
||||
function loadSettings() {
|
||||
const settings = {{ settings | tojson }};
|
||||
|
||||
// Metronom-Modus setzen
|
||||
document.querySelector(`input[name="metronomeMode"][value="${settings.metronome_mode}"]`).checked = true;
|
||||
|
||||
// Metronom-Geschwindigkeit setzen
|
||||
document.getElementById('metronomeSpeed').value = settings.metronome_speed;
|
||||
document.getElementById('speedValue').textContent = settings.metronome_speed;
|
||||
|
||||
// Fehlerrate setzen
|
||||
document.getElementById('targetErrorRate').value = settings.target_error_rate;
|
||||
document.getElementById('errorRateValue').textContent = settings.target_error_rate;
|
||||
|
||||
// Maximale BPM-Geschwindigkeit setzen
|
||||
document.getElementById('maxBpmSpeed').value = settings.max_bpm_speed;
|
||||
document.getElementById('maxBpmValue').textContent = settings.max_bpm_speed;
|
||||
|
||||
// Geschwindigkeitsanzeige setzen
|
||||
document.querySelector(`input[name="speedDisplay"][value="${settings.speed_display}"]`).checked = true;
|
||||
|
||||
// Sichtbarkeit der Einstellungen aktualisieren
|
||||
const explicitSettings = document.getElementById('explicitSettings');
|
||||
const automaticSettings = document.getElementById('automaticSettings');
|
||||
if (settings.metronome_mode === 'explicit') {
|
||||
explicitSettings.style.display = 'block';
|
||||
automaticSettings.style.display = 'none';
|
||||
} else {
|
||||
explicitSettings.style.display = 'none';
|
||||
automaticSettings.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Theme initialisieren
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
lucide.createIcons();
|
||||
initializeTheme();
|
||||
loadSettings();
|
||||
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue