feat(timer): Timer/Countdown-Widget mit Presets und Alarm
Countdown-Timer als Single-Instance-Widget mit Preset-System (max. 5), Web Audio API Alarm und Tab-Titel-Blink bei Ablauf. Mute-Toggle zum Stummschalten des Alarms. Z-Index-Hierarchie für Widgets auf 100 angehoben.
This commit is contained in:
@@ -70,6 +70,9 @@
|
|||||||
<button class="widget-toolbar-btn" data-action="calculator" title="Taschenrechner">
|
<button class="widget-toolbar-btn" data-action="calculator" title="Taschenrechner">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><circle cx="8" cy="10" r="0.5"/><circle cx="12" cy="10" r="0.5"/><circle cx="16" cy="10" r="0.5"/><circle cx="8" cy="14" r="0.5"/><circle cx="12" cy="14" r="0.5"/><circle cx="16" cy="14" r="0.5"/><circle cx="8" cy="18" r="0.5"/><circle cx="12" cy="18" r="0.5"/><circle cx="16" cy="18" r="0.5"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><circle cx="8" cy="10" r="0.5"/><circle cx="12" cy="10" r="0.5"/><circle cx="16" cy="10" r="0.5"/><circle cx="8" cy="14" r="0.5"/><circle cx="12" cy="14" r="0.5"/><circle cx="16" cy="14" r="0.5"/><circle cx="8" cy="18" r="0.5"/><circle cx="12" cy="18" r="0.5"/><circle cx="16" cy="18" r="0.5"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="widget-toolbar-btn" data-action="timer" title="Timer">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/><path d="M5 3l2 2"/><path d="M19 3l-2 2"/><line x1="12" y1="1" x2="12" y2="3"/></svg>
|
||||||
|
</button>
|
||||||
<button class="widget-toolbar-btn" data-action="notebook" title="Alle Notes">
|
<button class="widget-toolbar-btn" data-action="notebook" title="Alle Notes">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -479,6 +482,7 @@
|
|||||||
<script src="src/js/widgets.js"></script>
|
<script src="src/js/widgets.js"></script>
|
||||||
<script src="src/js/notes.js"></script>
|
<script src="src/js/notes.js"></script>
|
||||||
<script src="src/js/calculator.js"></script>
|
<script src="src/js/calculator.js"></script>
|
||||||
|
<script src="src/js/timer.js"></script>
|
||||||
<script src="src/js/data.js"></script>
|
<script src="src/js/data.js"></script>
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
<script src="src/js/onboarding.js"></script>
|
<script src="src/js/onboarding.js"></script>
|
||||||
|
|||||||
+210
-2
@@ -908,7 +908,7 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
============================================ */
|
============================================ */
|
||||||
.widget {
|
.widget {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 51;
|
z-index: 100;
|
||||||
min-width: 200px; min-height: 150px;
|
min-width: 200px; min-height: 150px;
|
||||||
background: var(--bg-board);
|
background: var(--bg-board);
|
||||||
border: 1px solid var(--border-accent);
|
border: 1px solid var(--border-accent);
|
||||||
@@ -1156,6 +1156,214 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
font-weight: 600;
|
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
|
WIDGET TOOLBAR
|
||||||
============================================ */
|
============================================ */
|
||||||
@@ -1164,7 +1372,7 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
right: 16px;
|
right: 16px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
z-index: 90;
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
+3
-1
@@ -19,6 +19,7 @@ async function init() {
|
|||||||
await migrateSticky();
|
await migrateSticky();
|
||||||
await Notes.init();
|
await Notes.init();
|
||||||
await Calculator.init();
|
await Calculator.init();
|
||||||
|
await Timer.init();
|
||||||
initDataButtons();
|
initDataButtons();
|
||||||
Store.checkQuota();
|
Store.checkQuota();
|
||||||
|
|
||||||
@@ -100,7 +101,8 @@ async function checkBackupReminder() {
|
|||||||
const widgetData = await Store.get('widgetStates');
|
const widgetData = await Store.get('widgetStates');
|
||||||
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
|
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
|
||||||
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
|
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 blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|||||||
+18
-2
@@ -18,7 +18,8 @@ function initDataButtons() {
|
|||||||
boards,
|
boards,
|
||||||
settings,
|
settings,
|
||||||
notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : [],
|
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 blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
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
|
// Gemeinsam speichern
|
||||||
await Store.set('widgetStates', existingWidgets);
|
await Store.set('widgetStates', existingWidgets);
|
||||||
|
|
||||||
const noteMsg = notesImported > 0 ? ` + ${notesImported} Note(s)` : '';
|
const noteMsg = notesImported > 0 ? ` + ${notesImported} Note(s)` : '';
|
||||||
const calcMsg = calcImported ? ' + Calculator-History' : '';
|
const calcMsg = calcImported ? ' + Calculator-History' : '';
|
||||||
|
const timerMsg = timerImported ? ' + Timer-Presets' : '';
|
||||||
await HellionDialog.alert(
|
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' }
|
{ type: 'success', title: 'Import erfolgreich' }
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
+6
-1
@@ -44,12 +44,15 @@ const Notes = {
|
|||||||
return note;
|
return note;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculator-State beibehalten falls vorhanden
|
// Calculator- und Timer-State beibehalten falls vorhanden
|
||||||
const existing = await Store.get(this.STORAGE_KEY);
|
const existing = await Store.get(this.STORAGE_KEY);
|
||||||
const saveData = { notes: merged };
|
const saveData = { notes: merged };
|
||||||
if (existing && existing.calculator) {
|
if (existing && existing.calculator) {
|
||||||
saveData.calculator = existing.calculator;
|
saveData.calculator = existing.calculator;
|
||||||
}
|
}
|
||||||
|
if (existing && existing.timer) {
|
||||||
|
saveData.timer = existing.timer;
|
||||||
|
}
|
||||||
await Store.set(this.STORAGE_KEY, saveData);
|
await Store.set(this.STORAGE_KEY, saveData);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -522,6 +525,8 @@ const Notes = {
|
|||||||
await this.create('checklist');
|
await this.create('checklist');
|
||||||
} else if (action === 'calculator') {
|
} else if (action === 'calculator') {
|
||||||
Calculator.toggle();
|
Calculator.toggle();
|
||||||
|
} else if (action === 'timer') {
|
||||||
|
Timer.toggle();
|
||||||
} else if (action === 'notebook') {
|
} else if (action === 'notebook') {
|
||||||
this.openNotebook();
|
this.openNotebook();
|
||||||
}
|
}
|
||||||
|
|||||||
+760
@@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
const WidgetManager = {
|
const WidgetManager = {
|
||||||
/** @type {Map<string, {el: HTMLElement, type: string, state: Object}>} */
|
/** @type {Map<string, {el: HTMLElement, type: string, state: Object}>} */
|
||||||
_widgets: new Map(),
|
_widgets: new Map(),
|
||||||
_topZ: 51,
|
_topZ: 100,
|
||||||
STORAGE_KEY: 'widgetStates',
|
STORAGE_KEY: 'widgetStates',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user