diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 246eb29..27963af 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,7 +25,7 @@ jobs:
run: |
mkdir -p dist
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip" \
- manifest.json newtab.html src/js/*.js src/css/ assets/ \
+ manifest.json newtab.html src/js/*.js src/css/ assets/ _locales/ \
-x "*.git*" "dist/*" ".github/*" "src/js/opera/*"
- name: Create Firefox ZIP (Manifest V3)
@@ -33,7 +33,7 @@ jobs:
cp manifest.json manifest.chrome-backup.json
cp manifest.firefox.json manifest.json
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip" \
- manifest.json newtab.html src/js/*.js src/css/ assets/ \
+ manifest.json newtab.html src/js/*.js src/css/ assets/ _locales/ \
-x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.firefox.json" "src/js/opera/*"
mv manifest.chrome-backup.json manifest.json
@@ -42,7 +42,7 @@ jobs:
cp manifest.json manifest.chrome-backup.json
cp manifest.opera.json manifest.json
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip" \
- manifest.json newtab.html src/js/*.js src/js/opera/ src/css/ assets/ \
+ manifest.json newtab.html src/js/*.js src/js/opera/ src/css/ assets/ _locales/ \
-x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.opera.json"
mv manifest.chrome-backup.json manifest.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79db7e6..9f142e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,29 @@ All notable changes per version. Format based on [Keep a Changelog](https://keep
---
+### v2.0.1 — 16.04.2026
+
+#### Security
+
+- **Background URL validation** — Only `blob:` and `data:image/` protocols allowed in CSS `backgroundImage` (prevents CSS injection via manipulated storage)
+- **Import URL validation** — `javascript:`, `data:`, and other unsafe protocols are blocked during JSON import
+- **Immutable import mapping** — Imported boards, bookmarks, and notes are sanitized with explicit field selection and string length limits
+
+#### Fixed
+
+- **Widget minimize race condition** — Replaced `setTimeout` with `transitionend` event; `openWidget()` during animation no longer causes display glitch
+- **Notes import mutation** — Import now uses `Notes.init()` instead of directly setting `Notes._notes`
+- **Complete i18n coverage** — 5 header button tooltips and 3 settings button texts now have `data-i18n` attributes (10 new translation keys)
+
+#### Changed
+
+- **Widget event system** — `WidgetManager` now dispatches `widget:close`, `widget:minimize`, `widget:open` CustomEvents via `EventTarget`. Calculator, Timer, and ImageRef use `WidgetManager.on()` instead of monkey-patching
+- **Local favicon icons** — Replaced Google Favicons API with local colored letter icons (deterministic hue per title). Zero external network requests, Brave Shields compatible
+- **backdrop-filter fallback** — `@supports not (backdrop-filter)` block with `--bg-solid-fallback` per theme for Brave Shields compatibility
+- **Clock interval cleanup** — `setInterval` ID stored in variable
+
+---
+
### v2.0.0 — 22.03.2026
#### New Features
diff --git a/manifest.firefox.json b/manifest.firefox.json
index 1bcae52..af05674 100644
--- a/manifest.firefox.json
+++ b/manifest.firefox.json
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
- "version": "2.0.0",
+ "version": "2.0.1",
"description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de",
diff --git a/manifest.json b/manifest.json
index ed2dfeb..bb4466e 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
- "version": "2.0.0",
+ "version": "2.0.1",
"description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de",
diff --git a/manifest.opera.json b/manifest.opera.json
index f0b9cbc..7cd00a5 100644
--- a/manifest.opera.json
+++ b/manifest.opera.json
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
- "version": "2.0.0",
+ "version": "2.0.1",
"description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de",
diff --git a/newtab.html b/newtab.html
index bdc7daf..df339a3 100644
--- a/newtab.html
+++ b/newtab.html
@@ -23,23 +23,23 @@
-
+
@@ -212,7 +212,7 @@
Alles zurücksetzen
Löscht alle Boards, Notes und Einstellungen
-
+
@@ -223,7 +223,7 @@
diff --git a/src/css/main.css b/src/css/main.css
index 33f0f3f..9f0045d 100644
--- a/src/css/main.css
+++ b/src/css/main.css
@@ -68,6 +68,19 @@
--board-hover-border: rgba(179,89,255,0.18);
--toggle-on-bg: rgba(214,92,255,0.22);
--logo-shadow: rgba(179,89,255,0.35);
+ --bg-solid-fallback: #0a060e;
+}
+
+/* Fallback fuer Browser die backdrop-filter blockieren (z.B. Brave Shields) */
+@supports not (backdrop-filter: blur(1px)) {
+ .board,
+ .widget,
+ .settings-panel,
+ .dialog-box,
+ .theme-modal,
+ .search-bar {
+ background-color: var(--bg-solid-fallback, var(--bg-primary));
+ }
}
/* ============================================
@@ -91,6 +104,7 @@
--board-hover-border: rgba(179,89,255,0.18);
--toggle-on-bg: rgba(214,92,255,0.22);
--logo-shadow: rgba(179,89,255,0.35);
+ --bg-solid-fallback: #0a060e;
}
[data-theme="nebula"] .board { border-color: rgba(214,92,255,0.10); }
[data-theme="nebula"] .bm-item:hover { background: rgba(214,92,255,0.05); }
@@ -116,6 +130,7 @@
--board-hover-border: rgba(212, 189, 138, 0.20);
--toggle-on-bg: rgba(200,168,74,0.22);
--logo-shadow: rgba(212, 189, 138, 0.40);
+ --bg-solid-fallback: #080c16;
letter-spacing: 0.5px;
}
@@ -146,6 +161,7 @@
--board-hover-border: rgba(157, 92, 255, 0.22);
--toggle-on-bg: rgba(224,128,48,0.22);
--logo-shadow: rgba(157, 92, 255, 0.45);
+ --bg-solid-fallback: #08050f;
}
[data-theme="event-horizon"] .board { border-color: rgba(157, 92, 255, 0.15); }
[data-theme="event-horizon"] .bm-item:hover { background: rgba(157, 92, 255, 0.08); }
@@ -172,6 +188,7 @@
--board-hover-border: rgba(46, 184, 184, 0.20);
--toggle-on-bg: rgba(78,207,207,0.22);
--logo-shadow: rgba(46, 184, 184, 0.45);
+ --bg-solid-fallback: #060a0a;
}
[data-theme="merchantman"] .board { border-color: rgba(46, 184, 184, 0.12); }
[data-theme="merchantman"] .bm-item:hover { background: rgba(46, 184, 184, 0.06); }
@@ -197,6 +214,7 @@
--board-hover-border: rgba(125, 179, 255, 0.22);
--toggle-on-bg: rgba(91,159,255,0.22);
--logo-shadow: rgba(125, 179, 255, 0.50);
+ --bg-solid-fallback: #070a14;
}
[data-theme="julia-jin"] .logo { font-family: 'Cinzel', serif; letter-spacing: 5px; text-transform: uppercase; }
[data-theme="julia-jin"] .clock { font-family: 'Cinzel', serif; font-weight: 600; }
@@ -225,6 +243,7 @@
--board-hover-border: rgba(255, 140, 61, 0.22);
--toggle-on-bg: rgba(240,124,48,0.22);
--logo-shadow: rgba(255, 140, 61, 0.45);
+ --bg-solid-fallback: #0f0a08;
}
[data-theme="sc-sunset"] .board {
border-color: rgba(255, 140, 61, 0.15);
@@ -253,6 +272,7 @@
--board-hover-border: rgba(50, 255, 106, 0.20);
--toggle-on-bg: rgba(34,204,68,0.20);
--logo-shadow: rgba(50, 255, 106, 0.40);
+ --bg-solid-fallback: #050805;
--danger: #ff4d4d;
}
[data-theme="hellion-hud"] .board {
@@ -287,6 +307,7 @@
--board-hover-border: rgba(30, 255, 142, 0.25);
--toggle-on-bg: rgba(0,232,122,0.18);
--logo-shadow: rgba(30, 255, 142, 0.60);
+ --bg-solid-fallback: #040705;
}
[data-theme="hellion-energy"] .board {
border-color: rgba(30, 255, 142, 0.15);
@@ -322,6 +343,7 @@
--board-hover-border: rgba(0, 180, 216, 0.25);
--toggle-on-bg: rgba(0, 180, 216, 0.20);
--logo-shadow: rgba(0, 180, 216, 0.40);
+ --bg-solid-fallback: #1a0f08;
}
[data-theme="satisfactory"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; }
[data-theme="satisfactory"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
@@ -352,6 +374,7 @@
--board-hover-border: rgba(46, 196, 160, 0.22);
--toggle-on-bg: rgba(46, 196, 160, 0.18);
--logo-shadow: rgba(46, 196, 160, 0.50);
+ --bg-solid-fallback: #020d0c;
}
[data-theme="avorion"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 500; letter-spacing: 6px; text-transform: uppercase; }
[data-theme="avorion"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 400; color: var(--accent); }
@@ -382,6 +405,7 @@
--board-hover-border: rgba(94, 194, 255, 0.25);
--toggle-on-bg: rgba(94, 194, 255, 0.20);
--logo-shadow: rgba(94, 194, 255, 0.45);
+ --bg-solid-fallback: #0d0f12;
}
[data-theme="hellion-stealth"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 4px; }
[data-theme="hellion-stealth"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
@@ -562,12 +586,13 @@ html, body {
body.compact .bm-item { padding: var(--spacing-compact) 10px; }
-.bm-favicon { width: 14px; height: 14px; flex-shrink: 0; border-radius: 2px; opacity: 0.85; }
-.bm-favicon-fallback {
- width: 14px; height: 14px; flex-shrink: 0;
- background: var(--accent-dim); border-radius: 2px;
+.bm-favicon-local {
+ width: 16px; height: 16px; flex-shrink: 0;
+ border-radius: 3px;
display: flex; align-items: center; justify-content: center;
- font-size: 8px; color: var(--accent);
+ font-size: 9px; font-weight: 600;
+ color: #fff;
+ line-height: 1;
}
.bm-text { flex: 1; min-width: 0; }
.bm-title { font-size: 12px; font-weight: 400; color: var(--text-primary); line-height: 1.3; }
diff --git a/src/js/app.js b/src/js/app.js
index dd16d25..9e143e4 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -105,7 +105,7 @@ async function checkBackupReminder() {
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : [];
- const data = { version: '2.0.0', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory, timerPresets };
+ const data = { version: '2.0.1', 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');
@@ -132,7 +132,7 @@ function startClock() {
`${t(DAY_KEYS[now.getDay()])}, ${String(now.getDate()).padStart(2,'0')}. ${t(MONTH_KEYS[now.getMonth()])}`;
}
tick();
- setInterval(tick, 1000);
+ const clockInterval = setInterval(tick, 1000);
}
// ---- GLOBALE EVENTS (Header-Buttons, Modals, Import) ----
diff --git a/src/js/boards.js b/src/js/boards.js
index 7e7d5fc..44aaa82 100644
--- a/src/js/boards.js
+++ b/src/js/boards.js
@@ -215,19 +215,11 @@ function createBmEl(bm) {
li.dataset.bmUrl = bm.url;
li.draggable = true;
- const favicon = document.createElement('img');
- favicon.className = 'bm-favicon';
- favicon.width = 14;
- favicon.height = 14;
- favicon.src = getFaviconUrl(bm.url);
- favicon.addEventListener('error', function() {
- this.classList.add('hidden');
- this.nextElementSibling.classList.remove('hidden');
- });
-
- const fallback = document.createElement('div');
- fallback.className = 'bm-favicon-fallback hidden';
- fallback.textContent = bm.title.charAt(0).toUpperCase();
+ const favicon = document.createElement('div');
+ favicon.className = 'bm-favicon-local';
+ favicon.textContent = bm.title.charAt(0).toUpperCase();
+ const hue = (bm.title.charCodeAt(0) * 137) % 360;
+ favicon.style.backgroundColor = `hsl(${hue}, 45%, 35%)`;
const textDiv = document.createElement('div');
textDiv.className = 'bm-text';
@@ -247,7 +239,6 @@ function createBmEl(bm) {
deleteBtn.textContent = '✕';
li.appendChild(favicon);
- li.appendChild(fallback);
li.appendChild(textDiv);
li.appendChild(deleteBtn);
diff --git a/src/js/calculator.js b/src/js/calculator.js
index b2f2f5b..f8090ed 100644
--- a/src/js/calculator.js
+++ b/src/js/calculator.js
@@ -689,41 +689,32 @@ const Calculator = {
await this.open();
}
- // Close-Event abfangen: WidgetManager.close() ueberschreiben
- const origClose = WidgetManager.close.bind(WidgetManager);
+ // Widget-Lifecycle-Events
const self = this;
- WidgetManager.close = function(id) {
- origClose(id);
- if (id === self.WIDGET_ID) {
+ WidgetManager.on('widget:close', (e) => {
+ if (e.detail.id === self.WIDGET_ID) {
self.onClose();
}
- };
+ });
- // Minimize-Event abfangen
- const origMinimize = WidgetManager.minimize.bind(WidgetManager);
- WidgetManager.minimize = async function(id) {
- await origMinimize(id);
- if (id === self.WIDGET_ID) {
+ WidgetManager.on('widget:minimize', (e) => {
+ if (e.detail.id === self.WIDGET_ID) {
self._isOpen = false;
- await self.save();
+ self.save();
}
- };
+ });
- // Open-Event abfangen
- const origOpen = WidgetManager.openWidget.bind(WidgetManager);
- WidgetManager.openWidget = async function(id) {
- await origOpen(id);
- if (id === self.WIDGET_ID) {
+ WidgetManager.on('widget:open', (e) => {
+ if (e.detail.id === self.WIDGET_ID) {
self._isOpen = true;
- // Body neu rendern (war durch minimize entfernt)
const body = WidgetManager.getBody(self.WIDGET_ID);
if (body && body.children.length === 0) {
self.renderBody(body);
}
const entry = WidgetManager._widgets.get(self.WIDGET_ID);
if (entry) self._bindKeyboard(entry.el);
- await self.save();
+ self.save();
}
- };
+ });
}
};
diff --git a/src/js/data.js b/src/js/data.js
index 4ccfb83..4b7c14a 100644
--- a/src/js/data.js
+++ b/src/js/data.js
@@ -9,11 +9,26 @@ function initDataButtons() {
const jsonInput = document.getElementById('jsonImportInput');
if (!btnExport || !btnImport) return;
+ /**
+ * Prueft ob eine URL ein sicheres Protokoll hat.
+ * Blockiert javascript:, data:, vbscript: etc.
+ * @param {string} url
+ * @returns {boolean}
+ */
+ function isSafeUrl(url) {
+ try {
+ const u = new URL(url);
+ return ['http:', 'https:', 'ftp:'].includes(u.protocol);
+ } catch {
+ return false;
+ }
+ }
+
// Export (inkl. Notes)
btnExport.addEventListener('click', async () => {
const widgetData = await Store.get('widgetStates');
const data = {
- version: '2.0.0',
+ version: '2.0.1',
exported: new Date().toISOString(),
boards,
settings,
@@ -38,18 +53,21 @@ function initDataButtons() {
try {
const data = JSON.parse(await file.text());
if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
- const validBoards = data.boards.filter(b => {
- if (!b || typeof b.title !== 'string' || !Array.isArray(b.bookmarks)) return false;
- b.id = b.id || uid();
- b.blurred = !!b.blurred;
- b.bookmarks = b.bookmarks.filter(bm => {
- if (!bm || typeof bm.title !== 'string' || typeof bm.url !== 'string') return false;
- bm.id = bm.id || uid();
- bm.desc = bm.desc || '';
- return true;
- });
- return true;
- });
+ const validBoards = data.boards
+ .filter(b => b && typeof b.title === 'string' && Array.isArray(b.bookmarks))
+ .map(b => ({
+ id: b.id || uid(),
+ title: String(b.title).slice(0, 100),
+ blurred: !!b.blurred,
+ bookmarks: b.bookmarks
+ .filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
+ .map(bm => ({
+ id: bm.id || uid(),
+ title: String(bm.title).slice(0, 200),
+ url: bm.url,
+ desc: String(bm.desc || '').slice(0, 500)
+ }))
+ }));
if (validBoards.length === 0) throw new Error(t('data.no_boards'));
const ok = await HellionDialog.confirm(
t('data.import_confirm', { count: validBoards.length }),
@@ -65,18 +83,26 @@ function initDataButtons() {
const existingWidgets = await Store.get('widgetStates') || {};
if (Array.isArray(data.notes) && data.notes.length > 0) {
const existingNotes = Array.isArray(existingWidgets.notes) ? existingWidgets.notes : [];
- const importNotes = data.notes.filter(n => {
- if (!n || !n.id || !n.template) return false;
- n.checklistItems = Array.isArray(n.checklistItems) ? n.checklistItems : [];
- return true;
- });
+ const importNotes = data.notes
+ .filter(n => n && n.id && n.template)
+ .map(n => ({
+ id: n.id,
+ template: ['note', 'checklist'].includes(n.template) ? n.template : 'note',
+ title: String(n.title || '').slice(0, 200),
+ content: String(n.content || '').slice(0, 5000),
+ x: typeof n.x === 'number' ? n.x : 120,
+ y: typeof n.y === 'number' ? n.y : 80,
+ width: typeof n.width === 'number' ? n.width : 280,
+ height: typeof n.height === 'number' ? n.height : 220,
+ open: n.open !== false,
+ checklistItems: Array.isArray(n.checklistItems) ? n.checklistItems : []
+ }));
// Limit beachten
const spaceLeft = Notes.MAX_NOTES - existingNotes.length;
const toImport = importNotes.slice(0, spaceLeft);
if (toImport.length > 0) {
const merged = [...existingNotes, ...toImport];
existingWidgets.notes = merged;
- Notes._notes = merged;
notesImported = toImport.length;
}
}
@@ -90,7 +116,6 @@ function initDataButtons() {
existingWidgets.calculator = { x: 400, y: 120, width: 280, height: 400, open: false, history: [] };
}
existingWidgets.calculator.history = calcHistory.slice(0, Calculator.MAX_HISTORY);
- Calculator._history = existingWidgets.calculator.history;
calcImported = true;
}
}
@@ -104,7 +129,6 @@ function initDataButtons() {
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;
}
}
@@ -112,6 +136,11 @@ function initDataButtons() {
// Gemeinsam speichern
await Store.set('widgetStates', existingWidgets);
+ // Widget-Module neu aus Storage laden (kein direkter Zugriff auf Internals)
+ if (notesImported > 0) await Notes.init();
+ if (calcImported) await Calculator.load();
+ if (timerImported) await Timer.load();
+
const noteMsg = notesImported > 0 ? t('data.notes_suffix', { count: notesImported }) : '';
const calcMsg = calcImported ? t('data.calc_suffix') : '';
const timerMsg = timerImported ? t('data.timer_suffix') : '';
diff --git a/src/js/i18n.js b/src/js/i18n.js
index 42486ea..313c299 100644
--- a/src/js/i18n.js
+++ b/src/js/i18n.js
@@ -202,6 +202,13 @@ const STRINGS = {
'header.theme': 'Darstellung',
'header.settings': 'Einstellungen',
+ // Header Tooltips
+ 'header.import_title': 'Bookmarks importieren (HTML)',
+ 'header.board_title': 'Neues Board hinzufügen',
+ 'header.note_title': 'Schnellnotiz',
+ 'header.theme_title': 'Darstellung & Theme',
+ 'header.settings_title': 'Einstellungen',
+
// Settings-Panel Überschrift
'settings.title': 'Einstellungen',
@@ -255,6 +262,13 @@ const STRINGS = {
'settings.bg_upload.desc': 'Lokales Bild als Hintergrund verwenden',
'settings.search_engine_toggle': 'Suchmaschine wechseln',
+ // Settings Buttons + Validierung
+ 'settings.onboarding_btn': 'Start',
+ 'settings.reset_btn': 'Reset',
+ 'settings.bg_upload_btn': 'Upload',
+ 'settings.bg_invalid_url': 'Nur lokale Bilder (Upload) sind als Hintergrund erlaubt.',
+ 'settings.bg_invalid_url.title': 'Ungültige URL',
+
// Modals
'modal.new_board': 'Neues Board',
'modal.board_name': 'Board-Name...',
@@ -493,6 +507,13 @@ const STRINGS = {
'header.theme': 'Theme',
'header.settings': 'Settings',
+ // Header Tooltips
+ 'header.import_title': 'Import bookmarks (HTML)',
+ 'header.board_title': 'Add new board',
+ 'header.note_title': 'Quick note',
+ 'header.theme_title': 'Appearance & Theme',
+ 'header.settings_title': 'Settings',
+
// Settings panel heading
'settings.title': 'Settings',
@@ -546,6 +567,13 @@ const STRINGS = {
'settings.bg_upload.desc': 'Use a local image as background',
'settings.search_engine_toggle': 'Switch search engine',
+ // Settings Buttons + Validation
+ 'settings.onboarding_btn': 'Start',
+ 'settings.reset_btn': 'Reset',
+ 'settings.bg_upload_btn': 'Upload',
+ 'settings.bg_invalid_url': 'Only local images (upload) are allowed as background.',
+ 'settings.bg_invalid_url.title': 'Invalid URL',
+
// Modals
'modal.new_board': 'New Board',
'modal.board_name': 'Board name...',
diff --git a/src/js/image-ref.js b/src/js/image-ref.js
index 93a2211..2f52433 100644
--- a/src/js/image-ref.js
+++ b/src/js/image-ref.js
@@ -460,41 +460,32 @@ const ImageRef = {
});
}
- // Close-Event abfangen
+ // Widget-Lifecycle-Events
const self = this;
- const prevClose = WidgetManager.close;
- WidgetManager.close = function(id) {
- prevClose.call(WidgetManager, id);
- // Pruefen ob es ein Image-Widget ist
- const isImage = self._images.some(img => img.id === id);
+ WidgetManager.on('widget:close', (e) => {
+ const isImage = self._images.some(img => img.id === e.detail.id);
if (isImage) {
- self.onClose(id);
+ self.onClose(e.detail.id);
}
- };
+ });
- // Minimize-Event abfangen
- const prevMinimize = WidgetManager.minimize;
- WidgetManager.minimize = async function(id) {
- await prevMinimize.call(WidgetManager, id);
- const isImage = self._images.some(img => img.id === id);
+ WidgetManager.on('widget:minimize', (e) => {
+ const isImage = self._images.some(img => img.id === e.detail.id);
if (isImage) {
- await self.save();
+ self.save();
}
- };
+ });
- // Open-Event abfangen
- const prevOpen = WidgetManager.openWidget;
- WidgetManager.openWidget = async function(id) {
- await prevOpen.call(WidgetManager, id);
- const imgData = self._images.find(img => img.id === id);
+ WidgetManager.on('widget:open', (e) => {
+ const imgData = self._images.find(img => img.id === e.detail.id);
if (imgData) {
- const body = WidgetManager.getBody(id);
+ const body = WidgetManager.getBody(e.detail.id);
if (body && body.children.length === 0) {
- const dataUrl = self._getSessionImage(id);
+ const dataUrl = self._getSessionImage(e.detail.id);
self.renderBody(imgData, body, dataUrl);
}
- await self.save();
+ self.save();
}
- };
+ });
}
};
diff --git a/src/js/settings.js b/src/js/settings.js
index 29408e2..b74e276 100644
--- a/src/js/settings.js
+++ b/src/js/settings.js
@@ -23,6 +23,17 @@ function closeThemeModal() {
overlay.classList.remove('active');
}
+/**
+ * Prueft ob eine Background-URL sicher fuer CSS-Einbettung ist.
+ * Erlaubt nur blob: und data:image/ Protokolle (aus File Upload).
+ * @param {string} url
+ * @returns {boolean}
+ */
+function isValidBgUrl(url) {
+ return typeof url === 'string' && url.length > 0 &&
+ (url.startsWith('blob:') || url.startsWith('data:image/'));
+}
+
// ---- ACCORDION ----
function initAccordion() {
const defaultOpen = new Set(['widgets']);
@@ -89,8 +100,10 @@ function applySettings() {
applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
- if (settings.bgUrl) {
+ if (settings.bgUrl && isValidBgUrl(settings.bgUrl)) {
document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`;
+ } else if (settings.bgUrl) {
+ settings.bgUrl = '';
}
}
@@ -168,6 +181,10 @@ function bindSettingsEvents() {
});
document.getElementById('btnApplyBg').addEventListener('click', async () => {
const url = document.getElementById('bgUrlInput').value.trim();
+ if (url && !isValidBgUrl(url)) {
+ await HellionDialog.alert(t('settings.bg_invalid_url'), { type: 'danger', title: t('settings.bg_invalid_url.title') });
+ return;
+ }
settings.bgUrl = url;
document.getElementById('bgLayer').style.backgroundImage = url ? `url('${url}')` : '';
await saveSettings();
@@ -183,6 +200,7 @@ function bindSettingsEvents() {
if (!file) return;
const reader = new FileReader();
reader.onload = async ev => {
+ if (!isValidBgUrl(ev.target.result)) return;
settings.bgUrl = ev.target.result;
document.getElementById('bgLayer').style.backgroundImage = `url('${ev.target.result}')`;
await saveSettings();
diff --git a/src/js/state.js b/src/js/state.js
index d387e47..d2db23e 100644
--- a/src/js/state.js
+++ b/src/js/state.js
@@ -33,15 +33,6 @@ function escHtml(str) {
.replace(/"/g, '"');
}
-function getFaviconUrl(url) {
- try {
- const u = new URL(url);
- return `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=16`;
- } catch {
- return '';
- }
-}
-
function getDefaultBoards() {
return [
{
diff --git a/src/js/timer.js b/src/js/timer.js
index 34ab404..8cb1616 100644
--- a/src/js/timer.js
+++ b/src/js/timer.js
@@ -720,32 +720,23 @@ const Timer = {
await this.open();
}
- // Close-Event abfangen
- const origClose = WidgetManager.close.bind(WidgetManager);
+ // Widget-Lifecycle-Events
const self = this;
- const prevClose = WidgetManager.close;
- WidgetManager.close = function(id) {
- prevClose.call(WidgetManager, id);
- if (id === self.WIDGET_ID) {
+ WidgetManager.on('widget:close', (e) => {
+ if (e.detail.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) {
+ WidgetManager.on('widget:minimize', (e) => {
+ if (e.detail.id === self.WIDGET_ID) {
self._isOpen = false;
- await self.save();
+ self.save();
}
- };
+ });
- // Open-Event abfangen
- const prevOpen = WidgetManager.openWidget;
- WidgetManager.openWidget = async function(id) {
- await prevOpen.call(WidgetManager, id);
- if (id === self.WIDGET_ID) {
+ WidgetManager.on('widget:open', (e) => {
+ if (e.detail.id === self.WIDGET_ID) {
self._isOpen = true;
const body = WidgetManager.getBody(self.WIDGET_ID);
if (body && body.children.length === 0) {
@@ -753,8 +744,8 @@ const Timer = {
}
const entry = WidgetManager._widgets.get(self.WIDGET_ID);
if (entry) self._bindKeyboard(entry.el);
- await self.save();
+ self.save();
}
- };
+ });
}
};
diff --git a/src/js/widgets.js b/src/js/widgets.js
index 47ea24d..b16ac56 100644
--- a/src/js/widgets.js
+++ b/src/js/widgets.js
@@ -9,6 +9,27 @@ const WidgetManager = {
_topZ: 100,
STORAGE_KEY: 'widgetStates',
+ /** @type {EventTarget} Internes Event-System fuer Widget-Lifecycle */
+ _emitter: new EventTarget(),
+
+ /**
+ * Event-Listener registrieren
+ * @param {string} event - z.B. 'widget:close', 'widget:minimize', 'widget:open'
+ * @param {Function} handler
+ */
+ on(event, handler) {
+ this._emitter.addEventListener(event, handler);
+ },
+
+ /**
+ * Event-Listener entfernen
+ * @param {string} event
+ * @param {Function} handler
+ */
+ off(event, handler) {
+ this._emitter.removeEventListener(event, handler);
+ },
+
/**
* Widget erstellen und in DOM einfuegen
* @param {string} type - 'note'
@@ -31,7 +52,7 @@ const WidgetManager = {
const el = this._buildDOM(state);
document.body.appendChild(el);
- this._widgets.set(id, { el, type, state });
+ this._widgets.set(id, { el, type, state, _minimizing: false });
this._initDrag(el);
this._initResize(el);
this.bringToFront(id);
@@ -144,22 +165,47 @@ const WidgetManager = {
const entry = this._widgets.get(id);
if (!entry) return;
entry.el.remove();
+ this._emitter.dispatchEvent(new CustomEvent('widget:close', { detail: { id } }));
this._widgets.delete(id);
},
/**
- * Widget minimieren (aus DOM verstecken, bleibt im Notebook)
+ * Widget minimieren (aus DOM verstecken, bleibt im Notebook).
+ * Nutzt transitionend statt setTimeout — _minimizing Flag verhindert Race Condition
+ * mit openWidget(). Fallback-Timer fuer prefers-reduced-motion / fehlende Transition.
* @param {string} id
*/
async minimize(id) {
const entry = this._widgets.get(id);
if (!entry) return;
entry.state.open = false;
+ entry._minimizing = true;
entry.el.classList.add('widget-minimized');
- setTimeout(() => {
- entry.el.style.display = 'none';
- }, 250);
+
+ const MINIMIZE_FALLBACK_MS = 350;
+
+ function onEnd(e) {
+ if (e.target !== entry.el || e.propertyName !== 'opacity') return;
+ clearTimeout(fallbackTimer);
+ entry.el.removeEventListener('transitionend', onEnd);
+ if (entry._minimizing) {
+ entry.el.style.display = 'none';
+ }
+ entry._minimizing = false;
+ }
+
+ entry.el.addEventListener('transitionend', onEnd);
+
+ const fallbackTimer = setTimeout(() => {
+ entry.el.removeEventListener('transitionend', onEnd);
+ if (entry._minimizing) {
+ entry.el.style.display = 'none';
+ entry._minimizing = false;
+ }
+ }, MINIMIZE_FALLBACK_MS);
+
await this.save();
+ this._emitter.dispatchEvent(new CustomEvent('widget:minimize', { detail: { id } }));
},
/**
@@ -169,14 +215,15 @@ const WidgetManager = {
async openWidget(id) {
const entry = this._widgets.get(id);
if (!entry) return;
+ entry._minimizing = false;
entry.state.open = true;
entry.el.style.display = 'flex';
- // Naechster Frame fuer Animation
requestAnimationFrame(() => {
entry.el.classList.remove('widget-minimized');
});
this.bringToFront(id);
await this.save();
+ this._emitter.dispatchEvent(new CustomEvent('widget:open', { detail: { id } }));
},
/**