From 37e45a20415eaebeac970a821ee2eaa37661c637 Mon Sep 17 00:00:00 2001 From: Florian Wathling Date: Sun, 22 Mar 2026 00:13:40 +0100 Subject: [PATCH] feat(calculator): Taschenrechner-Widget mit History und Tastatureingabe Neues Widget-Modul mit Shunting-Yard Parser, 4x5 Button-Grid, persistenter History (max 10) und Keyboard-Support. Storage-Handling in Notes/Data erweitert fuer parallele Persistierung. --- newtab.html | 4 + src/css/main.css | 102 ++++++ src/js/app.js | 4 +- src/js/calculator.js | 729 +++++++++++++++++++++++++++++++++++++++++++ src/js/data.js | 31 +- src/js/notes.js | 10 +- 6 files changed, 872 insertions(+), 8 deletions(-) create mode 100644 src/js/calculator.js diff --git a/newtab.html b/newtab.html index 205cb0b..a412981 100644 --- a/newtab.html +++ b/newtab.html @@ -67,6 +67,9 @@ + @@ -475,6 +478,7 @@ + diff --git a/src/css/main.css b/src/css/main.css index 615b432..070f5fb 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -1056,6 +1056,106 @@ body.show-desc .bm-desc { display: block; } opacity: 0.5; } +/* ============================================ + CALCULATOR WIDGET + ============================================ */ +.calc-display { + background: rgba(0,0,0,0.3); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px 12px; + margin-bottom: 8px; + text-align: right; + min-height: 48px; +} +.calc-expression { + font-size: 12px; + color: var(--text-muted); + min-height: 16px; + word-break: break-all; +} +.calc-result { + font-size: 22px; + font-family: 'Rajdhani', monospace; + color: var(--text-primary); + font-weight: 600; + word-break: break-all; +} +.calc-buttons { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 4px; + margin-bottom: 8px; +} +.calc-btn { + height: 36px; + background: rgba(255,255,255,0.04); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 14px; + font-family: 'Rajdhani', sans-serif; + cursor: pointer; + transition: all 0.1s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} +.calc-btn:hover { + background: rgba(255,255,255,0.08); + border-color: var(--border-accent); +} +.calc-btn:active { + transform: scale(0.95); +} +.calc-btn.operator { + color: var(--accent); + font-weight: 600; +} +.calc-btn.equals { + background: var(--accent-dim); + border-color: var(--accent); + color: var(--accent); + font-weight: 700; +} +.calc-btn.equals:hover { + background: var(--accent); + color: var(--bg-primary); +} +.calc-btn.clear { + color: var(--danger); +} +.calc-history { + border-top: 1px solid var(--border); + padding-top: 6px; + max-height: 100px; + overflow-y: auto; +} +.calc-history-title { + font-size: 9px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 4px; +} +.calc-history-item { + font-size: 11px; + color: var(--text-secondary); + padding: 3px 6px; + border-radius: var(--radius-sm); + cursor: pointer; + display: flex; + justify-content: space-between; +} +.calc-history-item:hover { + background: rgba(255,255,255,0.04); +} +.calc-history-item .calc-h-result { + color: var(--accent); + font-weight: 600; +} + /* ============================================ WIDGET TOOLBAR ============================================ */ @@ -1492,6 +1592,7 @@ body.show-desc .bm-desc { display: block; } .search-bar { max-width: 400px; } .widget-toolbar-btn { width: 32px; height: 32px; } .notebook-panel { width: 320px; } + .calc-btn { height: 32px; font-size: 13px; } } /* Smartphone (max 480px) */ @@ -1528,6 +1629,7 @@ body.show-desc .bm-desc { display: block; } .toolbar-left .widget-toolbar { left: 50%; right: auto; transform: translateX(-50%); } .widget-toolbar-btn { width: 32px; height: 32px; } .notebook-panel { width: 100%; right: -100%; } + .calc-btn { height: 30px; font-size: 12px; } .toolbar-left .notebook-panel { left: -100%; } .modal { width: calc(100vw - 32px); } diff --git a/src/js/app.js b/src/js/app.js index e2ac866..ce3690f 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -18,6 +18,7 @@ async function init() { initSearch(); await migrateSticky(); await Notes.init(); + await Calculator.init(); initDataButtons(); Store.checkQuota(); @@ -98,7 +99,8 @@ async function checkBackupReminder() { // JSON-Export auslösen (gleiche Logik wie btnExportJSON) const widgetData = await Store.get('widgetStates'); const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : []; - const data = { version: '1.6.0', exported: new Date().toISOString(), boards, settings, notes: notesData }; + const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : []; + const data = { version: '1.7.0', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); diff --git a/src/js/calculator.js b/src/js/calculator.js new file mode 100644 index 0000000..7b8f71e --- /dev/null +++ b/src/js/calculator.js @@ -0,0 +1,729 @@ +/* ============================================= + HELLION NEWTAB — calculator.js + Taschenrechner Widget: Expression-Parsing, + History, Tastatureingabe + ============================================= */ + +const Calculator = { + WIDGET_ID: 'widget_calculator', + STORAGE_KEY: 'widgetStates', + MAX_HISTORY: 10, + + /** @type {Array<{expr: string, result: string}>} */ + _history: [], + _currentExpr: '', + _lastResult: '', + _isOpen: false, + _displayExprEl: null, + _displayResultEl: null, + _keydownHandler: null, + + // ---- STORAGE ---- + + /** + * Calculator-State aus Storage laden + */ + async load() { + const data = await Store.get(this.STORAGE_KEY); + if (data && data.calculator) { + this._history = Array.isArray(data.calculator.history) ? data.calculator.history : []; + } + }, + + /** + * Calculator-State in Storage speichern + * Bestehende Notes-Daten bleiben erhalten + */ + async save() { + const data = await Store.get(this.STORAGE_KEY) || {}; + const notesState = Array.isArray(data.notes) ? data.notes : []; + + // Widget-Position aus WidgetManager holen + const widgetState = WidgetManager.getState(this.WIDGET_ID); + const calcData = { + x: widgetState ? widgetState.x : 400, + y: widgetState ? widgetState.y : 120, + width: widgetState ? widgetState.width : 280, + height: widgetState ? widgetState.height : 400, + open: this._isOpen, + history: this._history.slice(0, this.MAX_HISTORY) + }; + + await Store.set(this.STORAGE_KEY, { notes: notesState, calculator: calcData }); + }, + + // ---- WIDGET LIFECYCLE ---- + + /** + * Calculator oeffnen oder in Vordergrund bringen + */ + async open() { + if (this._isOpen) { + WidgetManager.bringToFront(this.WIDGET_ID); + return; + } + + // Gespeicherte Position laden + const data = await Store.get(this.STORAGE_KEY); + const saved = (data && data.calculator) ? data.calculator : {}; + + const widgetId = WidgetManager.create('calculator', { + id: this.WIDGET_ID, + title: 'Taschenrechner', + x: saved.x || 400, + y: saved.y || 120, + width: saved.width || 280, + height: saved.height || 400, + open: true + }); + + const body = WidgetManager.getBody(widgetId); + if (body) this.renderBody(body); + + this._isOpen = true; + + // Keyboard-Events binden + const entry = WidgetManager._widgets.get(this.WIDGET_ID); + if (entry) this._bindKeyboard(entry.el); + + await this.save(); + }, + + /** + * Calculator toggle: oeffnen oder minimieren + */ + async toggle() { + if (this._isOpen) { + const entry = WidgetManager._widgets.get(this.WIDGET_ID); + if (entry && entry.state.open) { + await WidgetManager.minimize(this.WIDGET_ID); + this._isOpen = false; + await this.save(); + } else if (entry) { + await WidgetManager.openWidget(this.WIDGET_ID); + this._isOpen = true; + await this.save(); + } + } else { + await this.open(); + } + }, + + /** + * Wird aufgerufen wenn Widget geschlossen wird + */ + async onClose() { + this._isOpen = false; + this._unbindKeyboard(); + this._displayExprEl = null; + this._displayResultEl = null; + await this.save(); + }, + + // ---- UI RENDERING ---- + + /** + * Calculator-Body rendern (in Widget-Body einfuegen) + * @param {HTMLElement} bodyEl + */ + renderBody(bodyEl) { + bodyEl.textContent = ''; + bodyEl.style.padding = '8px'; + bodyEl.style.display = 'flex'; + bodyEl.style.flexDirection = 'column'; + + // Display + const display = document.createElement('div'); + display.className = 'calc-display'; + + const exprEl = document.createElement('div'); + exprEl.className = 'calc-expression'; + this._displayExprEl = exprEl; + + const resultEl = document.createElement('div'); + resultEl.className = 'calc-result'; + resultEl.textContent = '0'; + this._displayResultEl = resultEl; + + display.append(exprEl, resultEl); + + // Buttons + const buttonsEl = this._createButtons(); + + // History + const historyEl = this._createHistoryPanel(); + + bodyEl.append(display, buttonsEl, historyEl); + + // Aktuellen State anzeigen + this._updateDisplay(); + }, + + /** + * Button-Grid erstellen (4x5) + * @returns {HTMLElement} + */ + _createButtons() { + const grid = document.createElement('div'); + grid.className = 'calc-buttons'; + + // Button-Layout: [label, value, cssClass] + const buttons = [ + ['C', 'clear', 'clear'], + ['()', 'paren', 'operator'], + ['%', '%', 'operator'], + ['\u00F7', '/', 'operator'], + ['7', '7', ''], + ['8', '8', ''], + ['9', '9', ''], + ['\u00D7', '*', 'operator'], + ['4', '4', ''], + ['5', '5', ''], + ['6', '6', ''], + ['\u2212', '-', 'operator'], + ['1', '1', ''], + ['2', '2', ''], + ['3', '3', ''], + ['+', '+', 'operator'], + ['0', '0', ''], + ['.', '.', ''], + ['\u232B', 'backspace', ''], + ['=', '=', 'equals'] + ]; + + buttons.forEach(([label, value, cls]) => { + const btn = document.createElement('button'); + btn.className = 'calc-btn' + (cls ? ' ' + cls : ''); + btn.textContent = label; + btn.type = 'button'; + btn.addEventListener('click', () => this._handleKey(value)); + grid.appendChild(btn); + }); + + return grid; + }, + + /** + * History-Panel erstellen + * @returns {HTMLElement} + */ + _createHistoryPanel() { + const container = document.createElement('div'); + container.className = 'calc-history'; + container.id = 'calcHistoryPanel'; + + const title = document.createElement('div'); + title.className = 'calc-history-title'; + title.textContent = 'History'; + container.appendChild(title); + + this._renderHistoryItems(container); + + return container; + }, + + /** + * History-Items rendern + * @param {HTMLElement} container + */ + _renderHistoryItems(container) { + // Alte Items entfernen (nur die .calc-history-item Elemente) + const oldItems = container.querySelectorAll('.calc-history-item'); + oldItems.forEach(item => item.remove()); + + if (this._history.length === 0) return; + + // Neueste zuerst + const reversed = [...this._history].reverse(); + reversed.forEach(entry => { + const item = document.createElement('div'); + item.className = 'calc-history-item'; + + const exprSpan = document.createElement('span'); + exprSpan.textContent = entry.expr; + + const resultSpan = document.createElement('span'); + resultSpan.className = 'calc-h-result'; + resultSpan.textContent = '= ' + entry.result; + + item.append(exprSpan, resultSpan); + + // Klick uebernimmt Ergebnis als neue Eingabe + item.addEventListener('click', () => { + this._currentExpr = entry.result; + this._lastResult = ''; + this._updateDisplay(); + }); + + container.appendChild(item); + }); + }, + + // ---- INPUT HANDLING ---- + + /** + * Taste verarbeiten + * @param {string} key + */ + _handleKey(key) { + switch (key) { + case 'clear': + this._currentExpr = ''; + this._lastResult = ''; + break; + + case 'backspace': + this._currentExpr = this._currentExpr.slice(0, -1); + break; + + case '=': + this._calculate(); + return; + + case 'paren': { + // Smarte Klammern: oeffnende wenn noetig, sonst schliessende + const openCount = (this._currentExpr.match(/\(/g) || []).length; + const closeCount = (this._currentExpr.match(/\)/g) || []).length; + const lastChar = this._currentExpr.slice(-1); + if (openCount <= closeCount || /[+\-*/%(]$/.test(lastChar) || this._currentExpr === '') { + this._currentExpr += '('; + } else { + this._currentExpr += ')'; + } + break; + } + + case '%': + case '+': + case '-': + case '*': + case '/': { + // Wenn gerade ein Ergebnis angezeigt wird, damit weiterrechnen + if (this._lastResult && this._currentExpr === '') { + this._currentExpr = this._lastResult; + this._lastResult = ''; + } + // Doppelte Operatoren verhindern (letzten ersetzen) + const last = this._currentExpr.slice(-1); + if (/[+\-*/%]/.test(last)) { + this._currentExpr = this._currentExpr.slice(0, -1) + key; + } else { + this._currentExpr += key; + } + break; + } + + case '.': { + // Doppelten Dezimalpunkt im letzten Zahlenblock verhindern + const parts = this._currentExpr.split(/[+\-*/%()]/); + const lastPart = parts[parts.length - 1]; + if (lastPart && lastPart.includes('.')) break; + this._currentExpr += key; + break; + } + + default: + // Ziffern 0-9 + if (/^[0-9]$/.test(key)) { + // Wenn ein Ergebnis da ist und User eine Zahl tippt, neue Berechnung starten + if (this._lastResult && this._currentExpr === '') { + this._lastResult = ''; + } + this._currentExpr += key; + } + break; + } + + this._updateDisplay(); + }, + + /** + * Berechnung ausfuehren + */ + async _calculate() { + if (!this._currentExpr) return; + + const result = this._evaluate(this._currentExpr); + if (result === null) { + this._lastResult = 'Fehler'; + this._updateDisplay(); + return; + } + + const resultStr = this._formatResult(result); + this._addHistory(this._currentExpr, resultStr); + this._lastResult = resultStr; + + // Display aktualisieren + if (this._displayExprEl) { + this._displayExprEl.textContent = this._formatExpression(this._currentExpr) + ' ='; + } + if (this._displayResultEl) { + this._displayResultEl.textContent = resultStr; + } + + this._currentExpr = ''; + + // History-Panel aktualisieren + const historyPanel = document.getElementById('calcHistoryPanel'); + if (historyPanel) this._renderHistoryItems(historyPanel); + + await this.save(); + }, + + // ---- EXPRESSION PARSER (Shunting-Yard, KEIN eval!) ---- + + /** + * Expression sicher auswerten + * @param {string} expr + * @returns {number|null} + */ + _evaluate(expr) { + try { + // Nur erlaubte Zeichen + const sanitized = expr.replace(/[^0-9+\-*/.%()]/g, ''); + if (!sanitized) return null; + + const tokens = this._tokenize(sanitized); + if (!tokens) return null; + + return this._parseExpression(tokens); + } catch { + return null; + } + }, + + /** + * Expression in Tokens aufteilen + * @param {string} expr + * @returns {Array|null} + */ + _tokenize(expr) { + const tokens = []; + let i = 0; + + while (i < expr.length) { + const ch = expr[i]; + + // Zahl (inkl. Dezimal) + if (/[0-9.]/.test(ch)) { + let num = ''; + while (i < expr.length && /[0-9.]/.test(expr[i])) { + num += expr[i]; + i++; + } + const parsed = parseFloat(num); + if (isNaN(parsed)) return null; + tokens.push({ type: 'number', value: parsed }); + continue; + } + + // Operator + if (/[+\-*/%]/.test(ch)) { + // Negativer Vorzeichen-Check: am Anfang oder nach Operator/oeffnender Klammer + if (ch === '-') { + const prev = tokens[tokens.length - 1]; + if (!prev || prev.type === 'op' || (prev.type === 'paren' && prev.value === '(')) { + // Negatives Vorzeichen → als Teil der naechsten Zahl lesen + let num = '-'; + i++; + while (i < expr.length && /[0-9.]/.test(expr[i])) { + num += expr[i]; + i++; + } + if (num === '-') return null; + const parsed = parseFloat(num); + if (isNaN(parsed)) return null; + tokens.push({ type: 'number', value: parsed }); + continue; + } + } + tokens.push({ type: 'op', value: ch }); + i++; + continue; + } + + // Klammern + if (ch === '(' || ch === ')') { + tokens.push({ type: 'paren', value: ch }); + i++; + continue; + } + + // Unbekanntes Zeichen + return null; + } + + return tokens; + }, + + /** + * Rekursiver Descent Parser mit Operator-Precedence + * @param {Array} tokens + * @returns {number|null} + */ + _parseExpression(tokens) { + let pos = 0; + + function peek() { return tokens[pos]; } + function consume() { return tokens[pos++]; } + + // Expression: Term (('+' | '-') Term)* + function parseExpr() { + let left = parseTerm(); + if (left === null) return null; + + while (pos < tokens.length) { + const t = peek(); + if (!t || t.type !== 'op' || (t.value !== '+' && t.value !== '-')) break; + consume(); + const right = parseTerm(); + if (right === null) return null; + left = t.value === '+' ? left + right : left - right; + } + return left; + } + + // Term: Factor (('*' | '/' | '%') Factor)* + function parseTerm() { + let left = parseFactor(); + if (left === null) return null; + + while (pos < tokens.length) { + const t = peek(); + if (!t || t.type !== 'op' || (t.value !== '*' && t.value !== '/' && t.value !== '%')) break; + consume(); + const right = parseFactor(); + if (right === null) return null; + if (t.value === '*') { + left = left * right; + } else if (t.value === '/') { + if (right === 0) return null; + left = left / right; + } else { + left = left % right; + } + } + return left; + } + + // Factor: Number | '(' Expression ')' + function parseFactor() { + const t = peek(); + if (!t) return null; + + if (t.type === 'number') { + consume(); + return t.value; + } + + if (t.type === 'paren' && t.value === '(') { + consume(); + const val = parseExpr(); + if (val === null) return null; + const closing = peek(); + if (closing && closing.type === 'paren' && closing.value === ')') { + consume(); + } + return val; + } + + return null; + } + + const result = parseExpr(); + + // Alle Tokens muessen verbraucht sein + if (pos < tokens.length) return null; + + if (result === null || !isFinite(result)) return null; + return result; + }, + + // ---- FORMATTING ---- + + /** + * Ergebnis formatieren (maximal 10 Dezimalstellen, trailing Nullen entfernen) + * @param {number} num + * @returns {string} + */ + _formatResult(num) { + if (Number.isInteger(num)) return num.toString(); + // Maximal 10 Dezimalstellen, trailing Nullen weg + const str = num.toFixed(10).replace(/\.?0+$/, ''); + return str; + }, + + /** + * Expression fuer Anzeige formatieren (× statt *, ÷ statt /) + * @param {string} expr + * @returns {string} + */ + _formatExpression(expr) { + return expr + .replace(/\*/g, '\u00D7') + .replace(/\//g, '\u00F7'); + }, + + // ---- DISPLAY ---- + + /** + * Display aktualisieren + */ + _updateDisplay() { + if (this._displayExprEl) { + if (this._lastResult) { + // Ergebnis-Modus: Expression oben, Ergebnis gross + // (wird von _calculate() direkt gesetzt) + } else { + this._displayExprEl.textContent = ''; + } + } + if (this._displayResultEl) { + if (this._lastResult && this._currentExpr === '') { + this._displayResultEl.textContent = this._lastResult; + } else { + this._displayResultEl.textContent = this._formatExpression(this._currentExpr) || '0'; + } + } + }, + + // ---- HISTORY ---- + + /** + * History-Eintrag hinzufuegen + * @param {string} expr + * @param {string} result + */ + _addHistory(expr, result) { + this._history.push({ + expr: this._formatExpression(expr), + result: result + }); + // Limit einhalten + if (this._history.length > this.MAX_HISTORY) { + this._history = this._history.slice(-this.MAX_HISTORY); + } + }, + + // ---- KEYBOARD ---- + + /** + * Tastatur-Events binden + * @param {HTMLElement} widgetEl + */ + _bindKeyboard(widgetEl) { + this._unbindKeyboard(); + + this._keydownHandler = (e) => { + // Nur reagieren wenn Calculator-Widget fokussiert ist + // (d.h. nicht wenn User in Textarea/Input tippt) + if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return; + if (e.target.contentEditable === 'true') return; + + const key = e.key; + let handled = false; + + if (/^[0-9]$/.test(key)) { + this._handleKey(key); + handled = true; + } else if (key === '+' || key === '-' || key === '*' || key === '/') { + this._handleKey(key); + handled = true; + } else if (key === '.') { + this._handleKey('.'); + handled = true; + } else if (key === '%') { + this._handleKey('%'); + handled = true; + } else if (key === '(' || key === ')') { + this._handleKey('paren'); + handled = true; + } else if (key === 'Enter' || key === '=') { + this._handleKey('='); + handled = true; + } else if (key === 'Backspace') { + this._handleKey('backspace'); + handled = true; + } else if (key === 'Escape' || key === 'c' || key === 'C') { + this._handleKey('clear'); + handled = true; + } + + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + }; + + widgetEl.addEventListener('keydown', this._keydownHandler); + // Widget fokussierbar machen + widgetEl.tabIndex = 0; + widgetEl.focus(); + }, + + /** + * Keyboard-Events entfernen + */ + _unbindKeyboard() { + if (this._keydownHandler) { + const entry = WidgetManager._widgets.get(this.WIDGET_ID); + if (entry) { + entry.el.removeEventListener('keydown', this._keydownHandler); + } + this._keydownHandler = null; + } + }, + + // ---- INIT ---- + + /** + * Calculator initialisieren (aus app.js aufgerufen) + */ + async init() { + await this.load(); + + // Wenn Calculator beim letzten Mal offen war, wiederherstellen + const data = await Store.get(this.STORAGE_KEY); + if (data && data.calculator && data.calculator.open) { + await this.open(); + } + + // Close-Event abfangen: WidgetManager.close() ueberschreiben + const origClose = WidgetManager.close.bind(WidgetManager); + const self = this; + WidgetManager.close = function(id) { + origClose(id); + if (id === self.WIDGET_ID) { + self.onClose(); + } + }; + + // Minimize-Event abfangen + const origMinimize = WidgetManager.minimize.bind(WidgetManager); + WidgetManager.minimize = async function(id) { + await origMinimize(id); + if (id === self.WIDGET_ID) { + self._isOpen = false; + await self.save(); + } + }; + + // Open-Event abfangen + const origOpen = WidgetManager.openWidget.bind(WidgetManager); + WidgetManager.openWidget = async function(id) { + await origOpen(id); + if (id === self.WIDGET_ID) { + self._isOpen = true; + // Body neu rendern (war durch minimize entfernt) + const body = WidgetManager.getBody(self.WIDGET_ID); + if (body && body.children.length === 0) { + self.renderBody(body); + } + const entry = WidgetManager._widgets.get(self.WIDGET_ID); + if (entry) self._bindKeyboard(entry.el); + await self.save(); + } + }; + } +}; diff --git a/src/js/data.js b/src/js/data.js index c51b42a..396e60e 100644 --- a/src/js/data.js +++ b/src/js/data.js @@ -13,11 +13,12 @@ function initDataButtons() { btnExport.addEventListener('click', async () => { const widgetData = await Store.get('widgetStates'); const data = { - version: '1.6.0', + version: '1.7.0', exported: new Date().toISOString(), boards, settings, - notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : [] + notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : [], + calculator: widgetData && widgetData.calculator ? widgetData.calculator.history || [] : [] }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -60,9 +61,9 @@ function initDataButtons() { // Notes importieren (falls vorhanden) let notesImported = 0; + const existingWidgets = await Store.get('widgetStates') || {}; if (Array.isArray(data.notes) && data.notes.length > 0) { - const existingWidgets = await Store.get('widgetStates'); - const existingNotes = (existingWidgets && Array.isArray(existingWidgets.notes)) ? existingWidgets.notes : []; + const existingNotes = Array.isArray(existingWidgets.notes) ? existingWidgets.notes : []; const importNotes = data.notes.filter(n => { if (!n || !n.id || !n.template) return false; n.checklistItems = Array.isArray(n.checklistItems) ? n.checklistItems : []; @@ -73,15 +74,33 @@ function initDataButtons() { const toImport = importNotes.slice(0, spaceLeft); if (toImport.length > 0) { const merged = [...existingNotes, ...toImport]; - await Store.set('widgetStates', { notes: merged }); + existingWidgets.notes = merged; Notes._notes = merged; notesImported = toImport.length; } } + // Calculator-History importieren (falls vorhanden) + let calcImported = false; + if (Array.isArray(data.calculator) && data.calculator.length > 0) { + const calcHistory = data.calculator.filter(h => h && typeof h.expr === 'string' && typeof h.result === 'string'); + if (calcHistory.length > 0) { + if (!existingWidgets.calculator) { + existingWidgets.calculator = { x: 400, y: 120, width: 280, height: 400, open: false, history: [] }; + } + existingWidgets.calculator.history = calcHistory.slice(0, Calculator.MAX_HISTORY); + Calculator._history = existingWidgets.calculator.history; + calcImported = true; + } + } + + // Gemeinsam speichern + await Store.set('widgetStates', existingWidgets); + const noteMsg = notesImported > 0 ? ` + ${notesImported} Note(s)` : ''; + const calcMsg = calcImported ? ' + Calculator-History' : ''; await HellionDialog.alert( - `${validBoards.length} Board(s)${noteMsg} erfolgreich importiert.`, + `${validBoards.length} Board(s)${noteMsg}${calcMsg} erfolgreich importiert.`, { type: 'success', title: 'Import erfolgreich' } ); } catch (err) { diff --git a/src/js/notes.js b/src/js/notes.js index bbbbce3..6e6a568 100644 --- a/src/js/notes.js +++ b/src/js/notes.js @@ -44,7 +44,13 @@ const Notes = { return note; }); - await Store.set(this.STORAGE_KEY, { notes: merged }); + // Calculator-State beibehalten falls vorhanden + const existing = await Store.get(this.STORAGE_KEY); + const saveData = { notes: merged }; + if (existing && existing.calculator) { + saveData.calculator = existing.calculator; + } + await Store.set(this.STORAGE_KEY, saveData); }, /** @@ -514,6 +520,8 @@ const Notes = { await this.create('text'); } else if (action === 'new-checklist') { await this.create('checklist'); + } else if (action === 'calculator') { + Calculator.toggle(); } else if (action === 'notebook') { this.openNotebook(); }