diff --git a/newtab.html b/newtab.html index a412981..d234357 100644 --- a/newtab.html +++ b/newtab.html @@ -70,6 +70,9 @@ + @@ -479,6 +482,7 @@ + diff --git a/src/css/main.css b/src/css/main.css index 070f5fb..53f4d2c 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -908,7 +908,7 @@ body.show-desc .bm-desc { display: block; } ============================================ */ .widget { position: fixed; - z-index: 51; + z-index: 100; min-width: 200px; min-height: 150px; background: var(--bg-board); border: 1px solid var(--border-accent); @@ -1156,6 +1156,214 @@ body.show-desc .bm-desc { display: block; } font-weight: 600; } +/* ============================================ + TIMER WIDGET + ============================================ */ +.timer-display { + text-align: center; + padding: 16px 0 8px; +} +.timer-time { + font-size: 36px; + font-family: 'Rajdhani', monospace; + font-weight: 700; + color: var(--text-primary); + letter-spacing: 2px; +} +.timer-time.finished { + color: var(--danger); + animation: timer-pulse 1s ease-in-out infinite; +} +@keyframes timer-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} +.timer-input-row { + display: flex; + justify-content: center; + margin-bottom: 12px; +} +.timer-input { + width: 120px; + text-align: center; + font-family: 'Rajdhani', monospace; + font-size: 16px; + background: rgba(0,0,0,0.3); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + padding: 6px 10px; +} +.timer-input::placeholder { color: var(--text-muted); } +.timer-input:focus { + outline: none; + border-color: var(--accent); +} +.timer-controls { + display: flex; + gap: 6px; + margin-bottom: 12px; + padding: 0 8px; +} +.timer-ctrl-btn { + flex: 1; + height: 32px; + background: rgba(255,255,255,0.04); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 12px; + font-family: 'Rajdhani', sans-serif; + cursor: pointer; + transition: all 0.1s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} +.timer-ctrl-btn:hover { + background: rgba(255,255,255,0.08); + border-color: var(--border-accent); +} +.timer-ctrl-btn:active { transform: scale(0.95); } +.timer-ctrl-btn:disabled { + opacity: 0.3; + cursor: default; + transform: none; +} +.timer-ctrl-btn.primary { + background: var(--accent-dim); + border-color: var(--accent); + color: var(--accent); + font-weight: 600; +} +.timer-ctrl-btn.primary:hover { + background: var(--accent); + color: var(--bg-primary); +} +.timer-ctrl-btn.danger { + color: var(--danger); +} +.timer-mute-btn { + width: 32px; + height: 32px; + flex: none; + font-size: 14px; + line-height: 1; + padding: 0; + background: var(--bg-board); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; +} +.timer-mute-btn:hover { + border-color: var(--accent); + color: var(--text-primary); +} +.timer-mute-btn.muted { + color: var(--text-muted); + opacity: 0.5; +} +.timer-presets { + border-top: 1px solid var(--border); + padding: 8px 8px 0; + max-height: 140px; + overflow-y: auto; +} +.timer-presets-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} +.timer-presets-title { + font-size: 9px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; +} +.timer-preset-add { + width: 22px; height: 22px; + background: none; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} +.timer-preset-add:hover { + border-color: var(--accent); + color: var(--accent); +} +.timer-preset-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 6px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 12px; + color: var(--text-secondary); +} +.timer-preset-item:hover { + background: rgba(255,255,255,0.04); +} +.timer-preset-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.timer-preset-time { + color: var(--accent); + font-family: 'Rajdhani', monospace; + margin: 0 8px; +} +.timer-preset-del { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 11px; + padding: 2px 4px; +} +.timer-preset-del:hover { color: var(--danger); } +.timer-add-row { + display: flex; + gap: 4px; + margin-top: 6px; +} +.timer-add-input { + flex: 1; + background: rgba(0,0,0,0.2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 11px; + padding: 4px 6px; +} +.timer-add-input::placeholder { color: var(--text-muted); } +.timer-add-input:focus { outline: none; border-color: var(--accent); } +.timer-add-confirm { + background: var(--accent-dim); + border: 1px solid var(--accent); + border-radius: var(--radius-sm); + color: var(--accent); + cursor: pointer; + font-size: 11px; + padding: 4px 8px; +} +.timer-add-confirm:hover { + background: var(--accent); + color: var(--bg-primary); +} + /* ============================================ WIDGET TOOLBAR ============================================ */ @@ -1164,7 +1372,7 @@ body.show-desc .bm-desc { display: block; } right: 16px; top: 50%; transform: translateY(-50%); - z-index: 90; + z-index: 100; display: flex; flex-direction: column; gap: 8px; diff --git a/src/js/app.js b/src/js/app.js index ce3690f..6316f82 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -19,6 +19,7 @@ async function init() { await migrateSticky(); await Notes.init(); await Calculator.init(); + await Timer.init(); initDataButtons(); Store.checkQuota(); @@ -100,7 +101,8 @@ async function checkBackupReminder() { const widgetData = await Store.get('widgetStates'); const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : []; 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 timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : []; + const data = { version: '1.7.0', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory, timerPresets }; 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/data.js b/src/js/data.js index 396e60e..1ecf3f1 100644 --- a/src/js/data.js +++ b/src/js/data.js @@ -18,7 +18,8 @@ function initDataButtons() { boards, settings, notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : [], - calculator: widgetData && widgetData.calculator ? widgetData.calculator.history || [] : [] + calculator: widgetData && widgetData.calculator ? widgetData.calculator.history || [] : [], + timerPresets: widgetData && widgetData.timer ? widgetData.timer.presets || [] : [] }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -94,13 +95,28 @@ function initDataButtons() { } } + // Timer-Presets importieren (falls vorhanden) + let timerImported = false; + if (Array.isArray(data.timerPresets) && data.timerPresets.length > 0) { + const validPresets = data.timerPresets.filter(p => p && typeof p.name === 'string' && typeof p.seconds === 'number'); + if (validPresets.length > 0) { + if (!existingWidgets.timer) { + existingWidgets.timer = { x: 600, y: 80, width: 260, height: 360, open: false, presets: [] }; + } + existingWidgets.timer.presets = validPresets.slice(0, Timer.MAX_PRESETS); + Timer._presets = existingWidgets.timer.presets; + timerImported = true; + } + } + // Gemeinsam speichern await Store.set('widgetStates', existingWidgets); const noteMsg = notesImported > 0 ? ` + ${notesImported} Note(s)` : ''; const calcMsg = calcImported ? ' + Calculator-History' : ''; + const timerMsg = timerImported ? ' + Timer-Presets' : ''; await HellionDialog.alert( - `${validBoards.length} Board(s)${noteMsg}${calcMsg} erfolgreich importiert.`, + `${validBoards.length} Board(s)${noteMsg}${calcMsg}${timerMsg} erfolgreich importiert.`, { type: 'success', title: 'Import erfolgreich' } ); } catch (err) { diff --git a/src/js/notes.js b/src/js/notes.js index 6e6a568..5f44caf 100644 --- a/src/js/notes.js +++ b/src/js/notes.js @@ -44,12 +44,15 @@ const Notes = { return note; }); - // Calculator-State beibehalten falls vorhanden + // Calculator- und Timer-State beibehalten falls vorhanden const existing = await Store.get(this.STORAGE_KEY); const saveData = { notes: merged }; if (existing && existing.calculator) { saveData.calculator = existing.calculator; } + if (existing && existing.timer) { + saveData.timer = existing.timer; + } await Store.set(this.STORAGE_KEY, saveData); }, @@ -522,6 +525,8 @@ const Notes = { await this.create('checklist'); } else if (action === 'calculator') { Calculator.toggle(); + } else if (action === 'timer') { + Timer.toggle(); } else if (action === 'notebook') { this.openNotebook(); } diff --git a/src/js/timer.js b/src/js/timer.js new file mode 100644 index 0000000..664ba73 --- /dev/null +++ b/src/js/timer.js @@ -0,0 +1,760 @@ +/* ============================================= + HELLION NEWTAB — timer.js + Timer / Countdown Widget: Presets, Alarm, + Tab-Titel-Blink + ============================================= */ + +const Timer = { + WIDGET_ID: 'widget_timer', + STORAGE_KEY: 'widgetStates', + MAX_PRESETS: 5, + + /** @type {Array<{name: string, seconds: number}>} */ + _presets: [], + _isOpen: false, + _seconds: 0, + _remaining: 0, + _intervalId: null, + _running: false, + _finished: false, + _blinkIntervalId: null, + _originalTitle: '', + _keydownHandler: null, + _muted: false, + + // UI-Referenzen + _timeEl: null, + _muteBtn: null, + _inputEl: null, + _inputRow: null, + _btnStart: null, + _btnPause: null, + _btnReset: null, + + // ---- STORAGE ---- + + /** + * Timer-State aus Storage laden + */ + async load() { + const data = await Store.get(this.STORAGE_KEY); + if (data && data.timer) { + this._presets = Array.isArray(data.timer.presets) ? data.timer.presets : []; + if (typeof data.timer.muted === 'boolean') this._muted = data.timer.muted; + } + }, + + /** + * Timer-State in Storage speichern + * Bestehende Notes + Calculator bleiben erhalten + */ + async save() { + const data = await Store.get(this.STORAGE_KEY) || {}; + if (data.notes === undefined) data.notes = []; + + const widgetState = WidgetManager.getState(this.WIDGET_ID); + data.timer = { + x: widgetState ? widgetState.x : 600, + y: widgetState ? widgetState.y : 80, + width: widgetState ? widgetState.width : 260, + height: widgetState ? widgetState.height : 360, + open: this._isOpen, + presets: this._presets.slice(0, this.MAX_PRESETS), + muted: this._muted + }; + + await Store.set(this.STORAGE_KEY, data); + }, + + // ---- WIDGET LIFECYCLE ---- + + /** + * Timer-Widget oeffnen oder in Vordergrund bringen + */ + async open() { + if (this._isOpen) { + WidgetManager.bringToFront(this.WIDGET_ID); + return; + } + + const data = await Store.get(this.STORAGE_KEY); + const saved = (data && data.timer) ? data.timer : {}; + + WidgetManager.create('timer', { + id: this.WIDGET_ID, + title: 'Timer', + x: saved.x || 600, + y: saved.y || 80, + width: saved.width || 260, + height: saved.height || 360, + open: true + }); + + const body = WidgetManager.getBody(this.WIDGET_ID); + if (body) this.renderBody(body); + + this._isOpen = true; + + const entry = WidgetManager._widgets.get(this.WIDGET_ID); + if (entry) this._bindKeyboard(entry.el); + + await this.save(); + }, + + /** + * Timer 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._stopCountdown(); + this._stopAlarm(); + this._timeEl = null; + this._inputEl = null; + this._inputRow = null; + this._btnStart = null; + this._btnPause = null; + this._btnReset = null; + this._muteBtn = null; + await this.save(); + }, + + // ---- UI RENDERING ---- + + /** + * Timer-Body rendern + * @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 = 'timer-display'; + + const timeEl = document.createElement('div'); + timeEl.className = 'timer-time'; + timeEl.textContent = '00:00'; + this._timeEl = timeEl; + display.appendChild(timeEl); + + // Input + const inputRow = document.createElement('div'); + inputRow.className = 'timer-input-row'; + this._inputRow = inputRow; + + const input = document.createElement('input'); + input.className = 'timer-input'; + input.type = 'text'; + input.placeholder = 'mm:ss'; + input.maxLength = 8; + this._inputEl = input; + + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this._applyInput(); + this._start(); + } + }); + + inputRow.appendChild(input); + + // Controls + const controls = document.createElement('div'); + controls.className = 'timer-controls'; + + const btnStart = document.createElement('button'); + btnStart.className = 'timer-ctrl-btn primary'; + btnStart.type = 'button'; + btnStart.textContent = 'Start'; + btnStart.addEventListener('click', () => { + if (!this._running && this._remaining === 0) { + this._applyInput(); + } + this._start(); + }); + this._btnStart = btnStart; + + const btnPause = document.createElement('button'); + btnPause.className = 'timer-ctrl-btn'; + btnPause.type = 'button'; + btnPause.textContent = 'Pause'; + btnPause.disabled = true; + btnPause.addEventListener('click', () => this._pause()); + this._btnPause = btnPause; + + const btnReset = document.createElement('button'); + btnReset.className = 'timer-ctrl-btn danger'; + btnReset.type = 'button'; + btnReset.textContent = 'Reset'; + btnReset.addEventListener('click', () => this._reset()); + this._btnReset = btnReset; + + controls.append(btnStart, btnPause, btnReset); + + // Mute Toggle (in Controls-Zeile) + const muteBtn = document.createElement('button'); + muteBtn.className = 'timer-mute-btn'; + muteBtn.type = 'button'; + this._muteBtn = muteBtn; + this._updateMuteBtn(); + muteBtn.addEventListener('click', async () => { + this._muted = !this._muted; + this._updateMuteBtn(); + await this.save(); + }); + controls.appendChild(muteBtn); + + // Presets + const presetsEl = this._createPresetsPanel(); + + bodyEl.append(display, inputRow, controls, presetsEl); + + // State wiederherstellen + this._updateDisplay(); + this._updateControls(); + }, + + /** + * Presets-Panel erstellen + * @returns {HTMLElement} + */ + _createPresetsPanel() { + const container = document.createElement('div'); + container.className = 'timer-presets'; + container.id = 'timerPresetsPanel'; + + const header = document.createElement('div'); + header.className = 'timer-presets-header'; + + const title = document.createElement('span'); + title.className = 'timer-presets-title'; + title.textContent = 'Presets'; + + const addBtn = document.createElement('button'); + addBtn.className = 'timer-preset-add'; + addBtn.type = 'button'; + addBtn.textContent = '+'; + addBtn.title = 'Preset speichern'; + addBtn.addEventListener('click', () => this._showAddPreset(container)); + + header.append(title, addBtn); + container.appendChild(header); + + this._renderPresetItems(container); + + return container; + }, + + /** + * Preset-Items rendern + * @param {HTMLElement} container + */ + _renderPresetItems(container) { + // Alte Items entfernen + const oldItems = container.querySelectorAll('.timer-preset-item, .timer-add-row'); + oldItems.forEach(item => item.remove()); + + this._presets.forEach((preset, idx) => { + const item = document.createElement('div'); + item.className = 'timer-preset-item'; + + const name = document.createElement('span'); + name.className = 'timer-preset-name'; + name.textContent = preset.name; + + const time = document.createElement('span'); + time.className = 'timer-preset-time'; + time.textContent = this._formatTime(preset.seconds); + + const del = document.createElement('button'); + del.className = 'timer-preset-del'; + del.type = 'button'; + del.textContent = '\u2715'; + del.addEventListener('click', async (e) => { + e.stopPropagation(); + await this._deletePreset(idx); + this._renderPresetItems(container); + }); + + item.append(name, time, del); + + // Klick laedt Preset + item.addEventListener('click', () => { + this._loadPreset(preset); + }); + + container.appendChild(item); + }); + }, + + /** + * Add-Preset UI anzeigen + * @param {HTMLElement} container + */ + _showAddPreset(container) { + // Nur einmal anzeigen + if (container.querySelector('.timer-add-row')) return; + + if (this._presets.length >= this.MAX_PRESETS) { + HellionDialog.alert( + 'Maximale Anzahl erreicht! Du kannst maximal ' + this.MAX_PRESETS + ' Presets speichern.', + { type: 'warning', title: 'Limit erreicht' } + ); + return; + } + + // Aktuelle Zeit als Vorlage + const currentSeconds = this._remaining > 0 ? this._seconds : 0; + if (currentSeconds === 0 && this._inputEl) { + const parsed = this._parseTimeInput(this._inputEl.value); + if (parsed === 0) { + HellionDialog.alert( + 'Gib zuerst eine Zeit ein, bevor du ein Preset speicherst.', + { type: 'info', title: 'Keine Zeit' } + ); + return; + } + } + + const row = document.createElement('div'); + row.className = 'timer-add-row'; + + const nameInput = document.createElement('input'); + nameInput.className = 'timer-add-input'; + nameInput.type = 'text'; + nameInput.placeholder = 'Name...'; + nameInput.maxLength = 20; + + const confirmBtn = document.createElement('button'); + confirmBtn.className = 'timer-add-confirm'; + confirmBtn.type = 'button'; + confirmBtn.textContent = 'OK'; + + const doAdd = async () => { + const name = nameInput.value.trim(); + if (!name) return; + + let secs = this._seconds; + if (secs === 0 && this._inputEl) { + secs = this._parseTimeInput(this._inputEl.value); + } + if (secs === 0) return; + + await this._addPreset(name, secs); + this._renderPresetItems(container); + }; + + confirmBtn.addEventListener('click', doAdd); + nameInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') doAdd(); + if (e.key === 'Escape') row.remove(); + }); + + row.append(nameInput, confirmBtn); + container.appendChild(row); + nameInput.focus(); + }, + + // ---- TIMER LOGIC ---- + + /** + * Input-Feld auslesen und als Sekunden setzen + */ + _applyInput() { + if (!this._inputEl) return; + const secs = this._parseTimeInput(this._inputEl.value); + if (secs > 0) { + this._seconds = secs; + this._remaining = secs; + } + }, + + /** + * Timer starten + */ + _start() { + if (this._running) return; + if (this._remaining <= 0) return; + + // Falls gerade Alarm laeuft, stoppen + if (this._finished) { + this._stopAlarm(); + this._finished = false; + } + + this._running = true; + this._updateControls(); + + // Input verstecken + if (this._inputRow) this._inputRow.style.display = 'none'; + + this._intervalId = setInterval(() => this._tick(), 1000); + }, + + /** + * Timer pausieren + */ + _pause() { + if (!this._running) return; + this._running = false; + this._stopCountdown(); + this._updateControls(); + }, + + /** + * Timer zuruecksetzen + */ + _reset() { + this._stopCountdown(); + this._stopAlarm(); + this._running = false; + this._finished = false; + this._remaining = 0; + this._seconds = 0; + + // Input wieder anzeigen + if (this._inputRow) this._inputRow.style.display = 'flex'; + if (this._inputEl) this._inputEl.value = ''; + + this._updateDisplay(); + this._updateControls(); + }, + + /** + * Jede Sekunde: remaining verringern, Display aktualisieren + */ + _tick() { + this._remaining--; + + if (this._remaining <= 0) { + this._remaining = 0; + this._stopCountdown(); + this._running = false; + this._finished = true; + this._onFinish(); + } + + this._updateDisplay(); + this._updateControls(); + }, + + /** + * Interval stoppen + */ + _stopCountdown() { + if (this._intervalId) { + clearInterval(this._intervalId); + this._intervalId = null; + } + }, + + /** + * Timer abgelaufen — Alarm + Tab-Blink + */ + _onFinish() { + if (!this._muted) this._playAlarm(); + this._startTitleBlink(); + }, + + /** + * Akustisches Signal (Browser Audio API, kein externer Request) + */ + _playAlarm() { + try { + const ctx = new AudioContext(); + [0, 0.3, 0.6].forEach(delay => { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.connect(gain); + gain.connect(ctx.destination); + osc.frequency.value = 800; + gain.gain.value = 0.07; + osc.start(ctx.currentTime + delay); + osc.stop(ctx.currentTime + delay + 0.2); + }); + } catch (e) { + console.warn('Timer: Audio nicht verfuegbar', e); + } + }, + + /** + * Tab-Titel blinken lassen + */ + _startTitleBlink() { + this._originalTitle = document.title; + this._blinkIntervalId = setInterval(() => { + document.title = document.title === '[!] Timer abgelaufen' + ? this._originalTitle + : '[!] Timer abgelaufen'; + }, 1000); + }, + + /** + * Tab-Titel Blink und Alarm stoppen + */ + _stopAlarm() { + if (this._blinkIntervalId) { + clearInterval(this._blinkIntervalId); + this._blinkIntervalId = null; + document.title = this._originalTitle || 'Hellion Dashboard'; + } + this._finished = false; + this._updateDisplay(); + this._updateControls(); + }, + + /** + * Mute-Button Text/Titel aktualisieren + */ + _updateMuteBtn() { + if (!this._muteBtn) return; + this._muteBtn.textContent = this._muted ? '\uD83D\uDD07' : '\uD83D\uDD0A'; + this._muteBtn.title = this._muted ? 'Ton einschalten' : 'Ton ausschalten'; + this._muteBtn.classList.toggle('muted', this._muted); + }, + + // ---- DISPLAY ---- + + /** + * Zeitanzeige aktualisieren + */ + _updateDisplay() { + if (!this._timeEl) return; + this._timeEl.textContent = this._formatTime(this._remaining); + this._timeEl.classList.toggle('finished', this._finished); + }, + + /** + * Button-States aktualisieren + */ + _updateControls() { + if (this._btnStart) { + this._btnStart.disabled = this._running; + this._btnStart.textContent = this._finished ? 'Neustart' : 'Start'; + } + if (this._btnPause) { + this._btnPause.disabled = !this._running; + } + }, + + // ---- PRESETS ---- + + /** + * Preset hinzufuegen + * @param {string} name + * @param {number} seconds + */ + async _addPreset(name, seconds) { + if (this._presets.length >= this.MAX_PRESETS) return; + this._presets.push({ name, seconds }); + await this.save(); + }, + + /** + * Preset loeschen + * @param {number} index + */ + async _deletePreset(index) { + this._presets.splice(index, 1); + await this.save(); + }, + + /** + * Preset laden (Zeit setzen) + * @param {Object} preset - { name, seconds } + */ + _loadPreset(preset) { + // Falls laufend, erst stoppen + this._stopCountdown(); + this._stopAlarm(); + this._running = false; + this._finished = false; + + this._seconds = preset.seconds; + this._remaining = preset.seconds; + + if (this._inputRow) this._inputRow.style.display = 'none'; + + this._updateDisplay(); + this._updateControls(); + }, + + // ---- FORMATTING ---- + + /** + * Sekunden in Zeitformat umwandeln + * @param {number} totalSeconds + * @returns {string} "05:30" oder "1:05:30" + */ + _formatTime(totalSeconds) { + const h = Math.floor(totalSeconds / 3600); + const m = Math.floor((totalSeconds % 3600) / 60); + const s = totalSeconds % 60; + + const mm = String(m).padStart(2, '0'); + const ss = String(s).padStart(2, '0'); + + if (h > 0) { + return h + ':' + mm + ':' + ss; + } + return mm + ':' + ss; + }, + + /** + * Zeit-String in Sekunden parsen + * Akzeptiert: "5:30", "05:30", "1:05:30", "90" (Sekunden) + * @param {string} str + * @returns {number} + */ + _parseTimeInput(str) { + const trimmed = (str || '').trim(); + if (!trimmed) return 0; + + const parts = trimmed.split(':'); + + if (parts.length === 1) { + // Nur Zahl = Sekunden + const secs = parseInt(parts[0], 10); + return isNaN(secs) ? 0 : Math.max(0, secs); + } + + if (parts.length === 2) { + // mm:ss + const m = parseInt(parts[0], 10); + const s = parseInt(parts[1], 10); + if (isNaN(m) || isNaN(s)) return 0; + return Math.max(0, m * 60 + s); + } + + if (parts.length === 3) { + // hh:mm:ss + const h = parseInt(parts[0], 10); + const m = parseInt(parts[1], 10); + const s = parseInt(parts[2], 10); + if (isNaN(h) || isNaN(m) || isNaN(s)) return 0; + return Math.max(0, h * 3600 + m * 60 + s); + } + + return 0; + }, + + // ---- KEYBOARD ---- + + /** + * Tastatur-Events binden + * @param {HTMLElement} widgetEl + */ + _bindKeyboard(widgetEl) { + this._unbindKeyboard(); + + this._keydownHandler = (e) => { + // Nicht reagieren wenn User in Input tippt + if (e.target.tagName === 'INPUT') return; + + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + if (this._running) { + this._pause(); + } else if (this._remaining > 0) { + this._start(); + } + } else if (e.key === 'Escape' || e.key === 'r' || e.key === 'R') { + e.preventDefault(); + this._reset(); + } + }; + + widgetEl.addEventListener('keydown', this._keydownHandler); + widgetEl.tabIndex = 0; + }, + + /** + * 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 ---- + + /** + * Timer initialisieren (aus app.js aufgerufen) + */ + async init() { + await this.load(); + + // Wenn Timer beim letzten Mal offen war, wiederherstellen + const data = await Store.get(this.STORAGE_KEY); + if (data && data.timer && data.timer.open) { + await this.open(); + } + + // Close-Event abfangen + const origClose = WidgetManager.close.bind(WidgetManager); + const self = this; + const prevClose = WidgetManager.close; + WidgetManager.close = function(id) { + prevClose.call(WidgetManager, id); + if (id === self.WIDGET_ID) { + self.onClose(); + } + }; + + // Minimize-Event abfangen + const prevMinimize = WidgetManager.minimize; + WidgetManager.minimize = async function(id) { + await prevMinimize.call(WidgetManager, id); + if (id === self.WIDGET_ID) { + self._isOpen = false; + await self.save(); + } + }; + + // Open-Event abfangen + const prevOpen = WidgetManager.openWidget; + WidgetManager.openWidget = async function(id) { + await prevOpen.call(WidgetManager, id); + if (id === self.WIDGET_ID) { + self._isOpen = true; + 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/widgets.js b/src/js/widgets.js index 5b7e9b4..d958bfc 100644 --- a/src/js/widgets.js +++ b/src/js/widgets.js @@ -6,7 +6,7 @@ const WidgetManager = { /** @type {Map} */ _widgets: new Map(), - _topZ: 51, + _topZ: 100, STORAGE_KEY: 'widgetStates', /**