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',
/**