diff --git a/README.md b/README.md index fb4b994..5d54e8f 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Die Webanwendung erreicht hervorragende Performance-Werte in allen Kategorien (P - Datum plus/minus X Wochen/Monate - Kalenderwoche zu Datum - Start-/Enddatum einer Kalenderwoche eines Jahres +- **Integrierter Taschenrechner** mit History und Sprachausgabe - Mehrsprachige Unterstützung (Deutsch/Englisch) mit automatischer Browser-Spracherkennung - Sprachausgabe für alle Ergebnisse (barrierefrei) - Statistik-Dashboard mit Passwortschutz unter `/stats` @@ -109,6 +110,7 @@ Die Anwendung unterstützt Deutsch und Englisch mit folgenden Features: - *Tastatur-Navigation*: Vollständig bedienbar - *ARIA-Attribute*: Korrekte Beschriftungen - *Semantische HTML*: Korrekte Struktur +- *Taschenrechner*: Vollständig barrierefrei mit Tastatur-Bedienung und Sprachausgabe ### *Technische Details:* - *Flask-Babel*: Professionelle i18n-Implementierung @@ -396,6 +398,7 @@ Elpatrons Datumsrechner ist als PWA installierbar (z.B. auf Android/iOS-Homescre - Manifest und Service Worker sind integriert - App-Icon und Theme-Color für Homescreen - Installation über Browser-Menü ("Zum Startbildschirm hinzufügen") +- Taschenrechner funktioniert vollständig clientseitig (offline verfügbar) ## Monitoring & Healthcheck @@ -449,7 +452,7 @@ Finde mal eine Datumsrechner- Webapp, die nicht völlig Werbe- und Tracking vers ### Vibe Coding -Dieses Projekt wurde zu nahezu 100% mit Unterstützung künsticher Intelligenz (*[Vibe Coding](https://de.wikipedia.org/wiki/Vibe_Coding)*) erstellt. Das Grundgerüst war nach ca. 45 Minuten fertig gestellt, insgesamt hat die Entwicklung des Projekts ca. 4 Stunden Zeit beansprucht. +Dieses Projekt wurde zu nahezu 100% mit Unterstützung künsticher Intelligenz (*[Vibe Coding](https://de.wikipedia.org/wiki/Vibe_Coding)*) erstellt. Das Grundgerüst war nach ca. 45 Minuten fertig gestellt, insgesamt hat die Entwicklung des Projekts ca. 12 Stunden Zeit beansprucht. ### Statistik-Erfassung, Logging @@ -466,6 +469,7 @@ Es werden keine IP-Adressen oder sonstigen persönlichen Daten gespeichert, ledi - *Farbkontraste:* Hohe Kontraste für Texte, Buttons und Ergebnisboxen, geprüft nach WCAG-Richtlinien. - *Status- und Fehlermeldungen:* Ergebnisse und Fehler werden mit `aria-live` für Screenreader zugänglich gemacht. - *Sprachausgabe:* Alle Ergebnisse können über 🔊-Buttons vorgelesen werden (Web Speech API, deutsche Sprache). +- *Taschenrechner:* Vollständig barrierefrei mit Tastatur-Bedienung, Sprachausgabe und History-Funktion. - *Mobile Optimierung:* Zusätzliche Meta-Tags für bessere Bedienbarkeit auf mobilen Geräten und Unterstützung von Screenreadern. - *SEO:* Das Thema Barrierefreiheit ist in den Meta-Tags für Suchmaschinen sichtbar. @@ -473,23 +477,23 @@ Damit ist die App für Menschen mit unterschiedlichen Einschränkungen (z.B. Seh ### Code Statistik -cloc|github.com/AlDanial/cloc v 2.06 T=0.17 s (146.7 files/s, 35235.5 lines/s) +cloc|github.com/AlDanial/cloc v 2.06 T=0.22 s (114.3 files/s, 32032.3 lines/s) --- | --- Language|files|blank|comment|code :-------|-------:|-------:|-------:|-------: -HTML|8|48|6|2092 -Python|2|59|68|690 -JavaScript|2|95|87|571 -Markdown|3|176|0|493 -PO File|2|234|240|492 +HTML|8|159|8|2800 +Python|2|66|74|739 +JavaScript|2|95|88|580 +PO File|2|260|266|544 +Markdown|3|177|0|497 JSON|3|0|0|243 CSS|1|186|3|188 SVG|2|0|0|14 Dockerfile|1|5|6|8 DOS Batch|1|0|0|1 --------|--------|--------|--------|-------- -SUM:|25|803|410|4792 +SUM:|25|948|445|5614 ## Lizenz @@ -498,4 +502,4 @@ Dieses Projekt steht unter der [MIT-Lizenz](LICENSE). --- (c) 2025 [Markus Busche](https://digitalcourage.social/@elpatron) -**Version 1.4.0** - Mehrsprachige Unterstützung hinzugefügt +**Version 1.4.12** - Integrierter Taschenrechner mit History und Sprachausgabe hinzugefügt diff --git a/app.py b/app.py index 224d55e..f032b53 100644 --- a/app.py +++ b/app.py @@ -20,7 +20,7 @@ app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations' babel = Babel() # Version der App -APP_VERSION = "1.4.11" +APP_VERSION = "1.4.12" def add_cache_headers(response): """Fügt Cache-Control-Header hinzu, die den Back-Forward-Cache ermöglichen""" diff --git a/templates/index.html b/templates/index.html index 4b9d74a..a479ca1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -97,6 +97,184 @@ body { background: rgba(37, 99, 235, 0.25); border-color: var(--primary); } +.help-button:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Calculator Styles */ +.calculator-btn { + background: var(--primary); + color: white; + border: none; + padding: 0.8em 1.5em; + border-radius: 8px; + font-size: 1em; + cursor: pointer; + transition: background-color 0.2s; + font-weight: 500; + width: 100%; + max-width: 480px; + min-height: 44px; + min-width: 44px; +} + +.calculator-btn:hover { + background: var(--primary-dark); +} + +.calculator-btn:focus { + outline: 3px solid #facc15; + outline-offset: 2px; + box-shadow: 0 0 0 4px #1e293b; +} + +.calculator-modal { + max-width: 400px; + width: fit-content; +} + +.calculator { + background: #f8fafc; + border-radius: 12px; + padding: 0.8em; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: fit-content; + margin: 0 auto; +} + +.calculator-display { + margin-bottom: 0.8em; +} + +.calculator-history { + margin-bottom: 0.8em; + min-height: 40px; + border: 2px solid #d1d5db; + border-radius: 6px; + background: #f9fafb; + padding: 0.3em; + font-family: 'Courier New', monospace; + font-size: 0.8em; + color: #6b7280; + overflow-y: auto; + max-height: 80px; +} + +.history-item { + padding: 0.3em 0; + border-bottom: 1px solid #e5e7eb; + text-align: right; + font-weight: 500; +} + +.history-item:last-child { + border-bottom: none; + color: #374151; + font-weight: 600; +} + +.calculator-display input { + width: 100%; + padding: 0.8em; + padding-right: 3.5em; + font-size: 1.3em; + text-align: right; + border: 3px solid #374151; + border-radius: 8px; + background: #ffffff; + color: #111827; + font-family: 'Courier New', monospace; + font-weight: 600; + min-height: 48px; +} + +.calculator-display input:focus { + outline: 3px solid #facc15; + outline-offset: 2px; + box-shadow: 0 0 0 4px #1e293b; + border-color: #1f2937; +} + +.calculator-buttons { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.3em; + margin: 0 auto; +} + +.calc-btn { + padding: 0.7em; + font-size: 1em; + border: 2px solid #374151; + background: #f9fafb; + color: #111827; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + font-weight: 600; + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; +} + +.calc-btn:hover { + background: #e5e7eb; + border-color: #1f2937; +} + +.calc-btn:focus { + outline: 3px solid #facc15; + outline-offset: 2px; + box-shadow: 0 0 0 4px #1e293b; + background: #e5e7eb; + border-color: #1f2937; +} + +.calc-clear { + background: #ef4444; + color: white; +} + +.calc-clear:hover { + background: #dc2626; +} + +.calc-delete { + background: #f59e0b; + color: white; +} + +.calc-delete:hover { + background: #d97706; +} + +.calc-operator { + background: var(--primary); + color: white; +} + +.calc-operator:hover { + background: var(--primary-dark); +} + +.calc-equals { + background: #10b981; + color: white; + grid-row: span 2; +} + +.calc-equals:hover { + background: #059669; +} + +.calc-zero { + grid-column: span 2; +} + .help-button:focus { outline: 3px solid #facc15; outline-offset: 2px; @@ -154,7 +332,7 @@ body { padding: 2em; max-width: 90%; width: 90%; - max-height: 90vh; + max-height: 95vh; overflow-y: auto; overflow-x: hidden; position: relative; @@ -508,7 +686,7 @@ button:focus, .accordion-header:focus { padding: 0.4em 0.6em; } .modal-content { - padding: 1.5em; + padding: 1.2em; margin: 1em; width: calc(100% - 2em); max-width: none; @@ -519,6 +697,35 @@ button:focus, .accordion-header:focus { overflow-x: hidden; } + .calculator-modal { + max-width: 95%; + width: 95%; + } + + .calculator { + width: 100%; + padding: 0.8em; + } + + .calculator-buttons { + width: 100%; + gap: 0.3em; + } + + .calc-btn { + padding: 0.6em; + font-size: 1em; + min-width: 44px; + min-height: 44px; + } + + .calculator-display input { + padding: 0.6em; + padding-right: 3em; + font-size: 1.2em; + min-height: 44px; + } + .modal-close { top: 0.8em; right: 0.8em; @@ -677,6 +884,214 @@ footer br + a { document.querySelector('.help-button').focus(); } + // Taschenrechner-Funktionen + let calculatorDisplay = '0'; + let calculatorPreviousValue = null; + let calculatorCurrentOperator = null; + let calculatorWaitingForOperand = false; + let calculatorHistory = []; + + function showCalculator() { + const modal = document.getElementById('calculatorModal'); + const input = document.getElementById('calculatorInput'); + + if (modal && input) { + modal.style.display = 'flex'; + modal.classList.add('active'); + input.focus(); + } + } + + function hideCalculator() { + const modal = document.getElementById('calculatorModal'); + modal.style.display = 'none'; + modal.classList.remove('active'); + } + + function calculatorClear() { + calculatorDisplay = '0'; + calculatorPreviousValue = null; + calculatorCurrentOperator = null; + calculatorWaitingForOperand = false; + calculatorHistory = []; + updateCalculatorDisplay(); + } + + function calculatorDelete() { + if (calculatorDisplay.length === 1) { + calculatorDisplay = '0'; + } else { + calculatorDisplay = calculatorDisplay.slice(0, -1); + } + updateCalculatorDisplay(); + } + + function calculatorNumber(num) { + if (calculatorWaitingForOperand) { + calculatorDisplay = num; + calculatorWaitingForOperand = false; + } else { + calculatorDisplay = calculatorDisplay === '0' ? num : calculatorDisplay + num; + } + updateCalculatorDisplay(); + } + + function calculatorDecimal() { + if (calculatorWaitingForOperand) { + calculatorDisplay = '0.'; + calculatorWaitingForOperand = false; + } else if (calculatorDisplay.indexOf('.') === -1) { + calculatorDisplay += '.'; + } + updateCalculatorDisplay(); + } + + function calculatorOperator(op) { + const inputValue = parseFloat(calculatorDisplay); + + if (calculatorPreviousValue === null) { + calculatorPreviousValue = inputValue; + } else if (calculatorCurrentOperator) { + const result = performCalculation(calculatorPreviousValue, inputValue, calculatorCurrentOperator); + calculatorDisplay = String(result); + calculatorPreviousValue = result; + } + + calculatorWaitingForOperand = true; + calculatorCurrentOperator = op; + updateCalculatorDisplay(); + } + + function calculatorEquals() { + const inputValue = parseFloat(calculatorDisplay); + + if (calculatorPreviousValue === null || calculatorCurrentOperator === null) { + return; + } + + const result = performCalculation(calculatorPreviousValue, inputValue, calculatorCurrentOperator); + + // Füge zur History hinzu + const historyEntry = calculatorPreviousValue + ' ' + getOperatorSymbol(calculatorCurrentOperator) + ' ' + inputValue + ' = ' + result; + addToHistory(historyEntry); + + calculatorDisplay = String(result); + calculatorPreviousValue = null; + calculatorCurrentOperator = null; + calculatorWaitingForOperand = true; + updateCalculatorDisplay(); + } + + function performCalculation(firstValue, secondValue, operator) { + switch (operator) { + case '+': + return firstValue + secondValue; + case '-': + return firstValue - secondValue; + case '*': + return firstValue * secondValue; + case '/': + return secondValue !== 0 ? firstValue / secondValue : 'Error'; + default: + return secondValue; + } + } + + function updateCalculatorDisplay() { + const display = document.getElementById('calculatorInput'); + if (calculatorDisplay === 'Error') { + display.value = 'Error'; + display.setAttribute('aria-label', '{{ _("Fehler") }}'); + calculatorDisplay = '0'; + calculatorPreviousValue = null; + calculatorCurrentOperator = null; + calculatorWaitingForOperand = false; + } else { + display.value = calculatorDisplay; + display.setAttribute('aria-label', '{{ _("Taschenrechner Anzeige") }}: ' + calculatorDisplay); + } + updateHistoryDisplay(); + } + + function readAloudFromCalculator(button) { + // Prüfe, ob bereits eine Wiedergabe läuft + if (currentSpeech && speechSynthesis.speaking) { + // Stoppe die aktuelle Wiedergabe + speechSynthesis.cancel(); + currentSpeech = null; + button.classList.remove('playing'); + button.textContent = '🔊'; + return; + } + + // Finde das Eingabefeld + const inputElement = document.getElementById('calculatorInput'); + if (!inputElement) return; + + let textToRead = inputElement.value; + + // Übersetze Zahlen und Operatoren für bessere Sprachausgabe + if (textToRead !== 'Error') { + // Prüfe, ob es sich um ein Ergebnis einer Berechnung handelt + if (calculatorHistory.length > 0) { + // Verwende die letzte Berechnung aus der History + const lastCalculation = calculatorHistory[0]; + textToRead = lastCalculation; + + // Übersetze mathematische Symbole für bessere Sprachausgabe + textToRead = textToRead.replace(/×/g, ' mal '); + textToRead = textToRead.replace(/÷/g, ' geteilt durch '); + textToRead = textToRead.replace(/−/g, ' minus '); + textToRead = textToRead.replace(/\+/g, ' plus '); + textToRead = textToRead.replace(/\./g, ' Komma '); + textToRead = textToRead.replace(/=/g, ' ist gleich '); + } else { + // Fallback für einzelne Zahlen + textToRead = textToRead.replace(/\./g, ' Komma '); + if (textToRead === '0') { + textToRead = '{{ _("Null") }}'; + } else { + textToRead = '{{ _("Taschenrechner Anzeige") }}: ' + textToRead; + } + } + } else { + textToRead = '{{ _("Fehler") }}'; + } + + readAloud(textToRead, button); + } + + function addToHistory(entry) { + calculatorHistory.unshift(entry); + if (calculatorHistory.length > 3) { + calculatorHistory.pop(); + } + } + + function updateHistoryDisplay() { + const historyContainer = document.getElementById('calculatorHistory'); + if (historyContainer) { + historyContainer.innerHTML = ''; + calculatorHistory.forEach((entry, index) => { + const historyItem = document.createElement('div'); + historyItem.className = 'history-item'; + historyItem.textContent = entry; + historyItem.setAttribute('aria-label', '{{ _("Berechnung") }} ' + (index + 1) + ': ' + entry); + historyContainer.appendChild(historyItem); + }); + } + } + + function getOperatorSymbol(operator) { + switch(operator) { + case '+': return '+'; + case '-': return '−'; + case '*': return '×'; + case '/': return '÷'; + default: return operator; + } + } + // Sprachausgabe-Funktionalität let currentSpeech = null; @@ -889,6 +1304,7 @@ footer br + a { document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { hideHelp(); + hideCalculator(); stopReading(); } }); @@ -899,6 +1315,101 @@ footer br + a { hideHelp(); } }); + + // Klick außerhalb des Taschenrechner-Modals zum Schließen + document.getElementById('calculatorModal').addEventListener('click', function(e) { + if (e.target === this) { + hideCalculator(); + } + }); + + // Tastatursteuerung für Taschenrechner-Button + document.getElementById('calculatorBtn').addEventListener('keydown', function(event) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + showCalculator(); + } + }); + + // Tastatursteuerung für Taschenrechner + document.addEventListener('keydown', function(event) { + const modal = document.getElementById('calculatorModal'); + if (modal.style.display === 'flex') { + switch(event.key) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + calculatorNumber(event.key); + event.preventDefault(); + break; + case '.': + case ',': + calculatorDecimal(); + event.preventDefault(); + break; + case '+': + case 'p': case 'P': // Plus + calculatorOperator('+'); + event.preventDefault(); + break; + case '-': + case 'm': case 'M': // Minus + calculatorOperator('-'); + event.preventDefault(); + break; + case '*': + case 'x': case 'X': // Multiplikation + case '×': + calculatorOperator('*'); + event.preventDefault(); + break; + case '/': + case 'd': case 'D': // Division + case '÷': + calculatorOperator('/'); + event.preventDefault(); + break; + case '=': + case 'Enter': + case ' ': // Leertaste für Gleich + calculatorEquals(); + event.preventDefault(); + break; + case 'Escape': + hideCalculator(); + event.preventDefault(); + break; + case 'Backspace': + case 'Delete': + calculatorDelete(); + event.preventDefault(); + break; + case 'c': case 'C': + case 'Escape': + calculatorClear(); + event.preventDefault(); + break; + case 'Tab': + // Erlaube normale Tab-Navigation + break; + default: + // Verhindere andere Tastatureingaben + event.preventDefault(); + break; + } + } + }); + + // Werktage-Checkbox Event-Handler + var werktageCheckbox = document.getElementById('werktage'); + var bundeslandSelect = document.getElementById('bundesland'); + if (werktageCheckbox && bundeslandSelect) { + function toggleBundesland() { + bundeslandSelect.disabled = !werktageCheckbox.checked; + } + werktageCheckbox.addEventListener('change', toggleBundesland); + // Initial setzen + toggleBundesland(); + } }); if ('serviceWorker' in navigator) { window.addEventListener('load', function() { @@ -1126,6 +1637,56 @@ footer br + a { + + +