1137 lines
33 KiB
JavaScript
1137 lines
33 KiB
JavaScript
/**
|
|
* Countdown-Timer mit Force-Quit-Funktionalität
|
|
*
|
|
* Dieses Modul stellt eine vollständige Timer-UI bereit mit:
|
|
* - Visueller Countdown-Anzeige
|
|
* - Fortschrittsbalken
|
|
* - Warnungen vor Ablauf
|
|
* - Force-Quit-Aktionen
|
|
* - Responsive Design
|
|
*
|
|
* Autor: System
|
|
* Erstellt: 2025
|
|
*/
|
|
|
|
class CountdownTimer {
|
|
/**
|
|
* Erstellt einen neuen Countdown-Timer
|
|
* @param {Object} options - Konfigurationsoptionen
|
|
*/
|
|
constructor(options = {}) {
|
|
// Standard-Konfiguration
|
|
this.config = {
|
|
// Timer-Grundeinstellungen
|
|
name: options.name || 'default_timer',
|
|
duration: options.duration || 1800, // 30 Minuten in Sekunden
|
|
autoStart: options.autoStart || false,
|
|
|
|
// UI-Einstellungen
|
|
container: options.container || 'countdown-timer',
|
|
size: options.size || 'large', // small, medium, large
|
|
theme: options.theme || 'primary', // primary, warning, danger, success
|
|
showProgress: options.showProgress !== false,
|
|
showControls: options.showControls !== false,
|
|
|
|
// Warnungs-Einstellungen
|
|
warningThreshold: options.warningThreshold || 30, // Warnung 30 Sekunden vor Ablauf
|
|
showWarning: options.showWarning !== false,
|
|
warningMessage: options.warningMessage || 'Timer läuft ab!',
|
|
|
|
// Force-Quit-Einstellungen
|
|
forceQuitEnabled: options.forceQuitEnabled !== false,
|
|
forceQuitAction: options.forceQuitAction || 'logout',
|
|
customEndpoint: options.customEndpoint || null,
|
|
|
|
// Callback-Funktionen
|
|
onTick: options.onTick || null,
|
|
onWarning: options.onWarning || null,
|
|
onExpired: options.onExpired || null,
|
|
onForceQuit: options.onForceQuit || null,
|
|
|
|
// API-Einstellungen
|
|
apiBase: options.apiBase || '/api/timers',
|
|
updateInterval: options.updateInterval || 1000,
|
|
syncWithServer: options.syncWithServer !== false
|
|
};
|
|
|
|
// Timer-Zustand
|
|
this.state = {
|
|
remaining: this.config.duration,
|
|
total: this.config.duration,
|
|
status: 'stopped', // stopped, running, paused, expired
|
|
warningShown: false,
|
|
lastServerSync: null
|
|
};
|
|
|
|
// DOM-Elemente
|
|
this.elements = {};
|
|
|
|
// Timer-Intervalle
|
|
this.intervals = {
|
|
countdown: null,
|
|
serverSync: null
|
|
};
|
|
|
|
// Event-Listener
|
|
this.listeners = new Map();
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialisiert den Timer
|
|
*/
|
|
init() {
|
|
this.createUI();
|
|
this.attachEventListeners();
|
|
|
|
if (this.config.syncWithServer) {
|
|
this.syncWithServer();
|
|
this.startServerSync();
|
|
}
|
|
|
|
if (this.config.autoStart) {
|
|
this.start();
|
|
}
|
|
|
|
console.log(`Timer '${this.config.name}' initialisiert`);
|
|
}
|
|
|
|
/**
|
|
* Erstellt die Timer-UI
|
|
*/
|
|
createUI() {
|
|
const container = document.getElementById(this.config.container);
|
|
if (!container) {
|
|
console.error(`Container '${this.config.container}' nicht gefunden`);
|
|
return;
|
|
}
|
|
|
|
// Haupt-Container erstellen
|
|
const timerWrapper = document.createElement('div');
|
|
timerWrapper.className = `countdown-timer-wrapper size-${this.config.size} theme-${this.config.theme}`;
|
|
timerWrapper.innerHTML = this.getTimerHTML();
|
|
|
|
container.appendChild(timerWrapper);
|
|
|
|
// DOM-Referenzen speichern
|
|
this.elements = {
|
|
wrapper: timerWrapper,
|
|
display: timerWrapper.querySelector('.timer-display'),
|
|
timeText: timerWrapper.querySelector('.time-text'),
|
|
progressBar: timerWrapper.querySelector('.progress-fill'),
|
|
progressText: timerWrapper.querySelector('.progress-text'),
|
|
statusIndicator: timerWrapper.querySelector('.status-indicator'),
|
|
warningBox: timerWrapper.querySelector('.warning-box'),
|
|
warningText: timerWrapper.querySelector('.warning-text'),
|
|
controls: timerWrapper.querySelector('.timer-controls'),
|
|
startBtn: timerWrapper.querySelector('.btn-start'),
|
|
pauseBtn: timerWrapper.querySelector('.btn-pause'),
|
|
stopBtn: timerWrapper.querySelector('.btn-stop'),
|
|
resetBtn: timerWrapper.querySelector('.btn-reset'),
|
|
extendBtn: timerWrapper.querySelector('.btn-extend')
|
|
};
|
|
|
|
this.updateDisplay();
|
|
}
|
|
|
|
/**
|
|
* Generiert das HTML für den Timer
|
|
*/
|
|
getTimerHTML() {
|
|
return `
|
|
<div class="countdown-timer-container">
|
|
<!-- Timer-Display -->
|
|
<div class="timer-display">
|
|
<div class="time-display">
|
|
<span class="time-text">${this.formatTime(this.state.remaining)}</span>
|
|
<span class="time-label">verbleibend</span>
|
|
</div>
|
|
<div class="status-indicator">
|
|
<i class="fas fa-circle"></i>
|
|
<span class="status-text">Gestoppt</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fortschrittsbalken -->
|
|
${this.config.showProgress ? `
|
|
<div class="progress-container">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: 0%"></div>
|
|
</div>
|
|
<div class="progress-text">0% abgelaufen</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- Warnungsbereich -->
|
|
<div class="warning-box" style="display: none;">
|
|
<div class="warning-content">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<span class="warning-text">${this.config.warningMessage}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Steuerungsbuttons -->
|
|
${this.config.showControls ? `
|
|
<div class="timer-controls">
|
|
<button class="btn btn-success btn-start" title="Timer starten">
|
|
<i class="fas fa-play"></i>
|
|
<span>Start</span>
|
|
</button>
|
|
<button class="btn btn-warning btn-pause" title="Timer pausieren" style="display: none;">
|
|
<i class="fas fa-pause"></i>
|
|
<span>Pause</span>
|
|
</button>
|
|
<button class="btn btn-danger btn-stop" title="Timer stoppen">
|
|
<i class="fas fa-stop"></i>
|
|
<span>Stop</span>
|
|
</button>
|
|
<button class="btn btn-secondary btn-reset" title="Timer zurücksetzen">
|
|
<i class="fas fa-redo"></i>
|
|
<span>Reset</span>
|
|
</button>
|
|
<button class="btn btn-info btn-extend" title="Timer verlängern">
|
|
<i class="fas fa-plus"></i>
|
|
<span>+5min</span>
|
|
</button>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Registriert Event-Listener
|
|
*/
|
|
attachEventListeners() {
|
|
if (this.elements.startBtn) {
|
|
this.elements.startBtn.addEventListener('click', () => this.start());
|
|
}
|
|
|
|
if (this.elements.pauseBtn) {
|
|
this.elements.pauseBtn.addEventListener('click', () => this.pause());
|
|
}
|
|
|
|
if (this.elements.stopBtn) {
|
|
this.elements.stopBtn.addEventListener('click', () => this.stop());
|
|
}
|
|
|
|
if (this.elements.resetBtn) {
|
|
this.elements.resetBtn.addEventListener('click', () => this.reset());
|
|
}
|
|
|
|
if (this.elements.extendBtn) {
|
|
this.elements.extendBtn.addEventListener('click', () => this.extend(300)); // 5 Minuten
|
|
}
|
|
|
|
// Globale Events für Tastatur-Shortcuts
|
|
document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e));
|
|
|
|
// Page Visibility API für Pause bei Tab-Wechsel
|
|
document.addEventListener('visibilitychange', () => this.handleVisibilityChange());
|
|
|
|
// Before Unload Event für Warnung
|
|
window.addEventListener('beforeunload', (e) => this.handleBeforeUnload(e));
|
|
}
|
|
|
|
/**
|
|
* Startet den Timer
|
|
*/
|
|
async start() {
|
|
try {
|
|
if (this.state.status === 'running') {
|
|
return true;
|
|
}
|
|
|
|
// Server-API aufrufen wenn aktiviert
|
|
if (this.config.syncWithServer) {
|
|
const response = await this.apiCall('start', 'POST');
|
|
if (!response.success) {
|
|
this.showError('Fehler beim Starten des Timers');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this.state.status = 'running';
|
|
this.startCountdown();
|
|
this.updateControls();
|
|
this.updateStatusIndicator();
|
|
|
|
console.log(`Timer '${this.config.name}' gestartet`);
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Starten des Timers:', error);
|
|
this.showError('Timer konnte nicht gestartet werden');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pausiert den Timer
|
|
*/
|
|
async pause() {
|
|
try {
|
|
if (this.state.status !== 'running') {
|
|
return true;
|
|
}
|
|
|
|
// Server-API aufrufen wenn aktiviert
|
|
if (this.config.syncWithServer) {
|
|
const response = await this.apiCall('pause', 'POST');
|
|
if (!response.success) {
|
|
this.showError('Fehler beim Pausieren des Timers');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this.state.status = 'paused';
|
|
this.stopCountdown();
|
|
this.updateControls();
|
|
this.updateStatusIndicator();
|
|
|
|
console.log(`Timer '${this.config.name}' pausiert`);
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Pausieren des Timers:', error);
|
|
this.showError('Timer konnte nicht pausiert werden');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stoppt den Timer
|
|
*/
|
|
async stop() {
|
|
try {
|
|
// Server-API aufrufen wenn aktiviert
|
|
if (this.config.syncWithServer) {
|
|
const response = await this.apiCall('stop', 'POST');
|
|
if (!response.success) {
|
|
this.showError('Fehler beim Stoppen des Timers');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this.state.status = 'stopped';
|
|
this.state.remaining = this.state.total;
|
|
this.state.warningShown = false;
|
|
|
|
this.stopCountdown();
|
|
this.hideWarning();
|
|
this.updateDisplay();
|
|
this.updateControls();
|
|
this.updateStatusIndicator();
|
|
|
|
console.log(`Timer '${this.config.name}' gestoppt`);
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Stoppen des Timers:', error);
|
|
this.showError('Timer konnte nicht gestoppt werden');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setzt den Timer zurück
|
|
*/
|
|
async reset() {
|
|
try {
|
|
// Server-API aufrufen wenn aktiviert
|
|
if (this.config.syncWithServer) {
|
|
const response = await this.apiCall('reset', 'POST');
|
|
if (!response.success) {
|
|
this.showError('Fehler beim Zurücksetzen des Timers');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this.stop();
|
|
this.state.remaining = this.state.total;
|
|
this.updateDisplay();
|
|
|
|
console.log(`Timer '${this.config.name}' zurückgesetzt`);
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Zurücksetzen des Timers:', error);
|
|
this.showError('Timer konnte nicht zurückgesetzt werden');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verlängert den Timer
|
|
*/
|
|
async extend(seconds) {
|
|
try {
|
|
// Server-API aufrufen wenn aktiviert
|
|
if (this.config.syncWithServer) {
|
|
const response = await this.apiCall('extend', 'POST', { seconds });
|
|
if (!response.success) {
|
|
this.showError('Fehler beim Verlängern des Timers');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this.state.remaining += seconds;
|
|
this.state.total += seconds;
|
|
this.state.warningShown = false;
|
|
|
|
this.hideWarning();
|
|
this.updateDisplay();
|
|
|
|
// Toast-Benachrichtigung
|
|
this.showToast(`Timer um ${Math.floor(seconds / 60)} Minuten verlängert`, 'success');
|
|
|
|
console.log(`Timer '${this.config.name}' um ${seconds} Sekunden verlängert`);
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Verlängern des Timers:', error);
|
|
this.showError('Timer konnte nicht verlängert werden');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Startet den Countdown-Mechanismus
|
|
*/
|
|
startCountdown() {
|
|
this.stopCountdown(); // Sicherstellen, dass kein anderer Countdown läuft
|
|
|
|
this.intervals.countdown = setInterval(() => {
|
|
this.tick();
|
|
}, this.config.updateInterval);
|
|
}
|
|
|
|
/**
|
|
* Stoppt den Countdown-Mechanismus
|
|
*/
|
|
stopCountdown() {
|
|
if (this.intervals.countdown) {
|
|
clearInterval(this.intervals.countdown);
|
|
this.intervals.countdown = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Timer-Tick (wird jede Sekunde aufgerufen)
|
|
*/
|
|
tick() {
|
|
if (this.state.status !== 'running') {
|
|
return;
|
|
}
|
|
|
|
this.state.remaining = Math.max(0, this.state.remaining - 1);
|
|
this.updateDisplay();
|
|
|
|
// Callback aufrufen
|
|
if (this.config.onTick) {
|
|
this.config.onTick(this.state.remaining, this.state.total);
|
|
}
|
|
|
|
// Warnung prüfen
|
|
if (!this.state.warningShown && this.state.remaining <= this.config.warningThreshold && this.state.remaining > 0) {
|
|
this.showWarning();
|
|
}
|
|
|
|
// Timer abgelaufen prüfen
|
|
if (this.state.remaining <= 0) {
|
|
this.handleExpired();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Behandelt Timer-Ablauf
|
|
*/
|
|
async handleExpired() {
|
|
console.warn(`Timer '${this.config.name}' ist abgelaufen`);
|
|
|
|
this.state.status = 'expired';
|
|
this.stopCountdown();
|
|
this.updateDisplay();
|
|
this.updateStatusIndicator();
|
|
|
|
// Callback aufrufen
|
|
if (this.config.onExpired) {
|
|
this.config.onExpired();
|
|
}
|
|
|
|
// Force-Quit ausführen wenn aktiviert
|
|
if (this.config.forceQuitEnabled) {
|
|
await this.executeForceQuit();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Führt Force-Quit-Aktion aus
|
|
*/
|
|
async executeForceQuit() {
|
|
try {
|
|
console.warn(`Force-Quit für Timer '${this.config.name}' wird ausgeführt...`);
|
|
|
|
// Callback aufrufen
|
|
if (this.config.onForceQuit) {
|
|
const shouldContinue = this.config.onForceQuit(this.config.forceQuitAction);
|
|
if (!shouldContinue) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Server-API aufrufen
|
|
if (this.config.syncWithServer) {
|
|
const response = await this.apiCall('force-quit', 'POST');
|
|
if (!response.success) {
|
|
console.error('Force-Quit-API-Aufruf fehlgeschlagen');
|
|
}
|
|
}
|
|
|
|
// Lokale Force-Quit-Aktionen
|
|
switch (this.config.forceQuitAction) {
|
|
case 'logout':
|
|
this.performLogout();
|
|
break;
|
|
case 'redirect':
|
|
this.performRedirect();
|
|
break;
|
|
case 'refresh':
|
|
this.performRefresh();
|
|
break;
|
|
case 'custom':
|
|
this.performCustomAction();
|
|
break;
|
|
default:
|
|
console.warn(`Unbekannte Force-Quit-Aktion: ${this.config.forceQuitAction}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Fehler bei Force-Quit-Ausführung:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Führt Logout-Aktion aus
|
|
*/
|
|
performLogout() {
|
|
this.showModal('Session abgelaufen', 'Sie werden automatisch abgemeldet...', 'warning');
|
|
|
|
setTimeout(() => {
|
|
window.location.href = '/auth/logout';
|
|
}, 2000);
|
|
}
|
|
|
|
/**
|
|
* Führt Redirect-Aktion aus
|
|
*/
|
|
performRedirect() {
|
|
const redirectUrl = this.config.redirectUrl || '/';
|
|
this.showModal('Umleitung', 'Sie werden weitergeleitet...', 'info');
|
|
|
|
setTimeout(() => {
|
|
window.location.href = redirectUrl;
|
|
}, 2000);
|
|
}
|
|
|
|
/**
|
|
* Führt Refresh-Aktion aus
|
|
*/
|
|
performRefresh() {
|
|
this.showModal('Seite wird aktualisiert', 'Die Seite wird automatisch neu geladen...', 'info');
|
|
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 2000);
|
|
}
|
|
|
|
/**
|
|
* Führt benutzerdefinierte Aktion aus
|
|
*/
|
|
performCustomAction() {
|
|
if (this.config.customEndpoint) {
|
|
fetch(this.config.customEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCSRFToken()
|
|
},
|
|
body: JSON.stringify({
|
|
timer_name: this.config.name,
|
|
action: 'force_quit'
|
|
})
|
|
}).catch(error => {
|
|
console.error('Custom-Action-Request fehlgeschlagen:', error);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zeigt Warnung an
|
|
*/
|
|
showWarning() {
|
|
if (!this.config.showWarning || this.state.warningShown) {
|
|
return;
|
|
}
|
|
|
|
this.state.warningShown = true;
|
|
|
|
if (this.elements.warningBox) {
|
|
this.elements.warningBox.style.display = 'block';
|
|
this.elements.warningBox.classList.add('pulse');
|
|
}
|
|
|
|
// Callback aufrufen
|
|
if (this.config.onWarning) {
|
|
this.config.onWarning(this.state.remaining);
|
|
}
|
|
|
|
// Browser-Benachrichtigung
|
|
this.showNotification('Timer-Warnung', this.config.warningMessage);
|
|
|
|
console.warn(`Timer-Warnung für '${this.config.name}': ${this.state.remaining} Sekunden verbleiben`);
|
|
}
|
|
|
|
/**
|
|
* Versteckt Warnung
|
|
*/
|
|
hideWarning() {
|
|
this.state.warningShown = false;
|
|
|
|
if (this.elements.warningBox) {
|
|
this.elements.warningBox.style.display = 'none';
|
|
this.elements.warningBox.classList.remove('pulse');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert die Anzeige
|
|
*/
|
|
updateDisplay() {
|
|
// Zeit-Text aktualisieren
|
|
if (this.elements.timeText) {
|
|
this.elements.timeText.textContent = this.formatTime(this.state.remaining);
|
|
}
|
|
|
|
// Fortschrittsbalken aktualisieren
|
|
if (this.config.showProgress && this.elements.progressBar) {
|
|
const progress = ((this.state.total - this.state.remaining) / this.state.total) * 100;
|
|
this.elements.progressBar.style.width = `${progress}%`;
|
|
|
|
if (this.elements.progressText) {
|
|
this.elements.progressText.textContent = `${Math.round(progress)}% abgelaufen`;
|
|
}
|
|
}
|
|
|
|
// Theme basierend auf verbleibender Zeit anpassen
|
|
this.updateTheme();
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert das Farbthema basierend auf verbleibender Zeit
|
|
*/
|
|
updateTheme() {
|
|
if (!this.elements.wrapper) return;
|
|
|
|
const progress = (this.state.total - this.state.remaining) / this.state.total;
|
|
|
|
// Theme-Klassen entfernen
|
|
this.elements.wrapper.classList.remove('theme-primary', 'theme-warning', 'theme-danger');
|
|
|
|
// Neues Theme basierend auf Fortschritt
|
|
if (progress < 0.7) {
|
|
this.elements.wrapper.classList.add('theme-primary');
|
|
} else if (progress < 0.9) {
|
|
this.elements.wrapper.classList.add('theme-warning');
|
|
} else {
|
|
this.elements.wrapper.classList.add('theme-danger');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert die Steuerungsbuttons
|
|
*/
|
|
updateControls() {
|
|
if (!this.config.showControls) return;
|
|
|
|
const isRunning = this.state.status === 'running';
|
|
const isPaused = this.state.status === 'paused';
|
|
const isStopped = this.state.status === 'stopped';
|
|
|
|
if (this.elements.startBtn) {
|
|
this.elements.startBtn.style.display = (isStopped || isPaused) ? 'inline-flex' : 'none';
|
|
}
|
|
|
|
if (this.elements.pauseBtn) {
|
|
this.elements.pauseBtn.style.display = isRunning ? 'inline-flex' : 'none';
|
|
}
|
|
|
|
if (this.elements.stopBtn) {
|
|
this.elements.stopBtn.disabled = isStopped;
|
|
}
|
|
|
|
if (this.elements.resetBtn) {
|
|
this.elements.resetBtn.disabled = isRunning;
|
|
}
|
|
|
|
if (this.elements.extendBtn) {
|
|
this.elements.extendBtn.disabled = this.state.status === 'expired';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert den Status-Indikator
|
|
*/
|
|
updateStatusIndicator() {
|
|
if (!this.elements.statusIndicator) return;
|
|
|
|
const statusText = this.elements.statusIndicator.querySelector('.status-text');
|
|
const statusIcon = this.elements.statusIndicator.querySelector('i');
|
|
|
|
if (statusText && statusIcon) {
|
|
switch (this.state.status) {
|
|
case 'running':
|
|
statusText.textContent = 'Läuft';
|
|
statusIcon.className = 'fas fa-circle text-success';
|
|
break;
|
|
case 'paused':
|
|
statusText.textContent = 'Pausiert';
|
|
statusIcon.className = 'fas fa-circle text-warning';
|
|
break;
|
|
case 'expired':
|
|
statusText.textContent = 'Abgelaufen';
|
|
statusIcon.className = 'fas fa-circle text-danger';
|
|
break;
|
|
default:
|
|
statusText.textContent = 'Gestoppt';
|
|
statusIcon.className = 'fas fa-circle text-secondary';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formatiert Zeit in MM:SS Format
|
|
*/
|
|
formatTime(seconds) {
|
|
const minutes = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
/**
|
|
* Synchronisiert mit Server
|
|
*/
|
|
async syncWithServer() {
|
|
try {
|
|
const response = await this.apiCall('status', 'GET');
|
|
if (response.success && response.data) {
|
|
const serverState = response.data;
|
|
|
|
// Lokalen Zustand mit Server-Zustand synchronisieren
|
|
this.state.remaining = serverState.remaining_seconds || this.state.remaining;
|
|
this.state.total = serverState.duration_seconds || this.state.total;
|
|
this.state.status = serverState.status || this.state.status;
|
|
|
|
this.updateDisplay();
|
|
this.updateControls();
|
|
this.updateStatusIndicator();
|
|
|
|
this.state.lastServerSync = new Date();
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Server-Synchronisation fehlgeschlagen:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Startet regelmäßige Server-Synchronisation
|
|
*/
|
|
startServerSync() {
|
|
if (this.intervals.serverSync) {
|
|
clearInterval(this.intervals.serverSync);
|
|
}
|
|
|
|
this.intervals.serverSync = setInterval(() => {
|
|
this.syncWithServer();
|
|
}, 30000); // Alle 30 Sekunden synchronisieren
|
|
}
|
|
|
|
/**
|
|
* API-Aufruf an Server
|
|
*/
|
|
async apiCall(action, method = 'GET', data = null) {
|
|
const url = `${this.config.apiBase}/${this.config.name}/${action}`;
|
|
|
|
const options = {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCSRFToken()
|
|
}
|
|
};
|
|
|
|
if (data && (method === 'POST' || method === 'PUT')) {
|
|
options.body = JSON.stringify(data);
|
|
}
|
|
|
|
const response = await fetch(url, options);
|
|
return await response.json();
|
|
}
|
|
|
|
/**
|
|
* Holt CSRF-Token
|
|
*/
|
|
getCSRFToken() {
|
|
const token = document.querySelector('meta[name="csrf-token"]');
|
|
return token ? token.getAttribute('content') : '';
|
|
}
|
|
|
|
/**
|
|
* Zeigt Browser-Benachrichtigung
|
|
*/
|
|
showNotification(title, message) {
|
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|
new Notification(title, {
|
|
body: message,
|
|
icon: '/static/icons/timer-icon.png'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zeigt Toast-Nachricht
|
|
*/
|
|
showToast(message, type = 'info') {
|
|
// Implementation für Toast-Nachrichten
|
|
console.log(`Toast [${type}]: ${message}`);
|
|
}
|
|
|
|
/**
|
|
* Zeigt Fehler-Modal
|
|
*/
|
|
showError(message) {
|
|
this.showModal('Fehler', message, 'danger');
|
|
}
|
|
|
|
/**
|
|
* Zeigt Modal-Dialog
|
|
*/
|
|
showModal(title, message, type = 'info') {
|
|
// Implementation für Modal-Dialoge
|
|
console.log(`Modal [${type}] ${title}: ${message}`);
|
|
}
|
|
|
|
/**
|
|
* Behandelt Tastatur-Shortcuts
|
|
*/
|
|
handleKeyboardShortcuts(e) {
|
|
if (e.ctrlKey || e.metaKey) {
|
|
switch (e.key) {
|
|
case ' ': // Strg + Leertaste
|
|
e.preventDefault();
|
|
if (this.state.status === 'running') {
|
|
this.pause();
|
|
} else {
|
|
this.start();
|
|
}
|
|
break;
|
|
case 'r': // Strg + R
|
|
e.preventDefault();
|
|
this.reset();
|
|
break;
|
|
case 's': // Strg + S
|
|
e.preventDefault();
|
|
this.stop();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Behandelt Visibility-Änderungen
|
|
*/
|
|
handleVisibilityChange() {
|
|
if (document.hidden) {
|
|
// Tab wurde versteckt
|
|
this.config._wasRunning = this.state.status === 'running';
|
|
} else {
|
|
// Tab wurde wieder sichtbar
|
|
if (this.config.syncWithServer) {
|
|
this.syncWithServer();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Behandelt Before-Unload-Event
|
|
*/
|
|
handleBeforeUnload(e) {
|
|
if (this.state.status === 'running' && this.state.remaining > 0) {
|
|
e.preventDefault();
|
|
e.returnValue = 'Timer läuft noch. Möchten Sie die Seite wirklich verlassen?';
|
|
return e.returnValue;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zerstört den Timer und räumt auf
|
|
*/
|
|
destroy() {
|
|
this.stopCountdown();
|
|
|
|
if (this.intervals.serverSync) {
|
|
clearInterval(this.intervals.serverSync);
|
|
}
|
|
|
|
// DOM-Elemente entfernen
|
|
if (this.elements.wrapper) {
|
|
this.elements.wrapper.remove();
|
|
}
|
|
|
|
// Event-Listener entfernen
|
|
this.listeners.forEach((listener, element) => {
|
|
element.removeEventListener(listener.event, listener.handler);
|
|
});
|
|
|
|
console.log(`Timer '${this.config.name}' zerstört`);
|
|
}
|
|
}
|
|
|
|
// CSS-Styles für den Timer (wird dynamisch eingefügt)
|
|
const timerStyles = `
|
|
<style id="countdown-timer-styles">
|
|
.countdown-timer-wrapper {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
max-width: 400px;
|
|
margin: 0 auto;
|
|
text-align: center;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
background: white;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.countdown-timer-container {
|
|
padding: 24px;
|
|
}
|
|
|
|
/* Größenvarianten */
|
|
.size-small { max-width: 250px; font-size: 0.9em; }
|
|
.size-medium { max-width: 350px; }
|
|
.size-large { max-width: 450px; font-size: 1.1em; }
|
|
|
|
/* Farbthemen */
|
|
.theme-primary { border-left: 4px solid #007bff; }
|
|
.theme-warning { border-left: 4px solid #ffc107; }
|
|
.theme-danger { border-left: 4px solid #dc3545; }
|
|
.theme-success { border-left: 4px solid #28a745; }
|
|
|
|
/* Timer-Display */
|
|
.timer-display {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.time-display {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.time-text {
|
|
font-size: 3em;
|
|
font-weight: bold;
|
|
color: #2c3e50;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.time-label {
|
|
display: block;
|
|
font-size: 0.9em;
|
|
color: #6c757d;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.status-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
font-size: 0.9em;
|
|
color: #6c757d;
|
|
}
|
|
|
|
/* Fortschrittsbalken */
|
|
.progress-container {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 8px;
|
|
background-color: #e9ecef;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #007bff, #0056b3);
|
|
transition: width 0.5s ease-in-out;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.progress-text {
|
|
font-size: 0.85em;
|
|
color: #6c757d;
|
|
}
|
|
|
|
/* Warnungsbereich */
|
|
.warning-box {
|
|
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
|
|
border: 1px solid #ffc107;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin-bottom: 20px;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.warning-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
color: #856404;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.warning-content i {
|
|
color: #ffc107;
|
|
}
|
|
|
|
/* Steuerungsbuttons */
|
|
.timer-controls {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.timer-controls .btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 0.9em;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.timer-controls .btn:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.timer-controls .btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.btn-success { background: #28a745; color: white; }
|
|
.btn-warning { background: #ffc107; color: #212529; }
|
|
.btn-danger { background: #dc3545; color: white; }
|
|
.btn-secondary { background: #6c757d; color: white; }
|
|
.btn-info { background: #17a2b8; color: white; }
|
|
|
|
/* Animationen */
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.7; }
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 480px) {
|
|
.countdown-timer-wrapper {
|
|
margin: 0 16px;
|
|
}
|
|
|
|
.time-text {
|
|
font-size: 2.5em;
|
|
}
|
|
|
|
.timer-controls {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.timer-controls .btn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
/* Dark Mode Support */
|
|
@media (prefers-color-scheme: dark) {
|
|
.countdown-timer-wrapper {
|
|
background: #2c3e50;
|
|
color: white;
|
|
}
|
|
|
|
.time-text {
|
|
color: #ecf0f1;
|
|
}
|
|
|
|
.progress-bar {
|
|
background-color: #34495e;
|
|
}
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
// Styles automatisch einfügen wenn noch nicht vorhanden
|
|
if (!document.getElementById('countdown-timer-styles')) {
|
|
document.head.insertAdjacentHTML('beforeend', timerStyles);
|
|
}
|
|
|
|
// Globale Funktionen für einfache Nutzung
|
|
window.CountdownTimer = CountdownTimer;
|
|
|
|
// Timer-Manager für mehrere Timer-Instanzen
|
|
window.TimerManager = {
|
|
timers: new Map(),
|
|
|
|
create(name, options) {
|
|
if (this.timers.has(name)) {
|
|
console.warn(`Timer '${name}' existiert bereits`);
|
|
return this.timers.get(name);
|
|
}
|
|
|
|
const timer = new CountdownTimer({
|
|
...options,
|
|
name: name
|
|
});
|
|
|
|
this.timers.set(name, timer);
|
|
return timer;
|
|
},
|
|
|
|
get(name) {
|
|
return this.timers.get(name);
|
|
},
|
|
|
|
destroy(name) {
|
|
const timer = this.timers.get(name);
|
|
if (timer) {
|
|
timer.destroy();
|
|
this.timers.delete(name);
|
|
}
|
|
},
|
|
|
|
destroyAll() {
|
|
this.timers.forEach(timer => timer.destroy());
|
|
this.timers.clear();
|
|
}
|
|
};
|