manage-your-printer/static/js/countdown-timer.js
2025-06-04 10:03:22 +02:00

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();
}
};