Merge branch 'feature/hardening-v2.0.1' — Hardening Release v2.0.1
Security: URL-Validierung (bgUrl + Import), immutable Data-Mapping Stability: Widget Event-System (EventTarget), transitionend Race Fix Privacy: Lokale Letter-Icons statt Google Favicons API Compat: backdrop-filter Fallback, _locales in Release-ZIPs i18n: 10 fehlende Übersetzungs-Keys ergänzt Quality: Clock Interval ID, Notes-Import über init()
This commit is contained in:
@@ -25,7 +25,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip" \
|
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/*"
|
-x "*.git*" "dist/*" ".github/*" "src/js/opera/*"
|
||||||
|
|
||||||
- name: Create Firefox ZIP (Manifest V3)
|
- name: Create Firefox ZIP (Manifest V3)
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
cp manifest.json manifest.chrome-backup.json
|
cp manifest.json manifest.chrome-backup.json
|
||||||
cp manifest.firefox.json manifest.json
|
cp manifest.firefox.json manifest.json
|
||||||
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip" \
|
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/*"
|
-x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.firefox.json" "src/js/opera/*"
|
||||||
mv manifest.chrome-backup.json manifest.json
|
mv manifest.chrome-backup.json manifest.json
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
cp manifest.json manifest.chrome-backup.json
|
cp manifest.json manifest.chrome-backup.json
|
||||||
cp manifest.opera.json manifest.json
|
cp manifest.opera.json manifest.json
|
||||||
zip -r "dist/hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip" \
|
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"
|
-x "*.git*" "dist/*" ".github/*" "manifest.chrome-backup.json" "manifest.opera.json"
|
||||||
mv manifest.chrome-backup.json manifest.json
|
mv manifest.chrome-backup.json manifest.json
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
### v2.0.0 — 22.03.2026
|
||||||
|
|
||||||
#### New Features
|
#### New Features
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"author": "Hellion Online Media - Florian Wathling",
|
"author": "Hellion Online Media - Florian Wathling",
|
||||||
"homepage_url": "https://hellion-media.de",
|
"homepage_url": "https://hellion-media.de",
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"author": "Hellion Online Media - Florian Wathling",
|
"author": "Hellion Online Media - Florian Wathling",
|
||||||
"homepage_url": "https://hellion-media.de",
|
"homepage_url": "https://hellion-media.de",
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"author": "Hellion Online Media - Florian Wathling",
|
"author": "Hellion Online Media - Florian Wathling",
|
||||||
"homepage_url": "https://hellion-media.de",
|
"homepage_url": "https://hellion-media.de",
|
||||||
|
|||||||
+9
-9
@@ -23,23 +23,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button class="btn-icon" id="btnImport" title="Bookmarks importieren (HTML)">
|
<button class="btn-icon" id="btnImport" title="Bookmarks importieren (HTML)" data-i18n-title="header.import_title">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||||
<span data-i18n="header.import">Import</span>
|
<span data-i18n="header.import">Import</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" id="btnAddBoard" title="Neues Board hinzufügen">
|
<button class="btn-icon" id="btnAddBoard" title="Neues Board hinzufügen" data-i18n-title="header.board_title">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
<span data-i18n="header.board">Board</span>
|
<span data-i18n="header.board">Board</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" id="btnNote" title="Schnellnotiz">
|
<button class="btn-icon" id="btnNote" title="Schnellnotiz" data-i18n-title="header.note_title">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||||
<span data-i18n="header.note">Note</span>
|
<span data-i18n="header.note">Note</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" id="btnTheme" title="Darstellung & Theme">
|
<button class="btn-icon" id="btnTheme" title="Darstellung & Theme" data-i18n-title="header.theme_title">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 100 20 4 4 0 01-1-7.9 1 1 0 011-.1h1a2 2 0 002-2V7a5 5 0 00-3-4.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="6" r="1.5"/><circle cx="17" cy="10" r="1.5"/><circle cx="9" cy="17" r="1.5"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 100 20 4 4 0 01-1-7.9 1 1 0 011-.1h1a2 2 0 002-2V7a5 5 0 00-3-4.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="6" r="1.5"/><circle cx="17" cy="10" r="1.5"/><circle cx="9" cy="17" r="1.5"/></svg>
|
||||||
<span data-i18n="header.theme">Darstellung</span>
|
<span data-i18n="header.theme">Darstellung</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" id="btnSettings" title="Einstellungen">
|
<button class="btn-icon" id="btnSettings" title="Einstellungen" data-i18n-title="header.settings_title">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
||||||
<span data-i18n="header.settings">Settings</span>
|
<span data-i18n="header.settings">Settings</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
<span class="setting-label" data-i18n="settings.onboarding">Onboarding wiederholen</span>
|
<span class="setting-label" data-i18n="settings.onboarding">Onboarding wiederholen</span>
|
||||||
<span class="setting-desc" data-i18n="settings.onboarding.desc">Willkommens-Tour erneut anzeigen</span>
|
<span class="setting-desc" data-i18n="settings.onboarding.desc">Willkommens-Tour erneut anzeigen</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-small" id="btnRestartOnboarding">Start</button>
|
<button class="btn-small" id="btnRestartOnboarding" data-i18n="settings.onboarding_btn">Start</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
<span class="setting-label" data-i18n="settings.reset">Alles zurücksetzen</span>
|
<span class="setting-label" data-i18n="settings.reset">Alles zurücksetzen</span>
|
||||||
<span class="setting-desc" data-i18n="settings.reset.desc">Löscht alle Boards, Notes und Einstellungen</span>
|
<span class="setting-desc" data-i18n="settings.reset.desc">Löscht alle Boards, Notes und Einstellungen</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-danger" id="btnResetAll">Reset</button>
|
<button class="btn-danger" id="btnResetAll" data-i18n="settings.reset_btn">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
<div class="about-block">
|
<div class="about-block">
|
||||||
<div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div>
|
<div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div>
|
||||||
<div class="about-version">Version 2.0.0 · by Hellion Online Media</div>
|
<div class="about-version">Version 2.0.1 · by Hellion Online Media</div>
|
||||||
|
|
||||||
<div class="about-links">
|
<div class="about-links">
|
||||||
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
|
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
|
||||||
@@ -371,7 +371,7 @@
|
|||||||
<span class="setting-label" data-i18n="settings.bg_upload">Datei hochladen</span>
|
<span class="setting-label" data-i18n="settings.bg_upload">Datei hochladen</span>
|
||||||
<span class="setting-desc" data-i18n="settings.bg_upload.desc">Lokales Bild als Hintergrund verwenden</span>
|
<span class="setting-desc" data-i18n="settings.bg_upload.desc">Lokales Bild als Hintergrund verwenden</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-small" id="btnBgFile">Upload</button>
|
<button class="btn-small" id="btnBgFile" data-i18n="settings.bg_upload_btn">Upload</button>
|
||||||
<input type="file" id="bgFileInput" accept="image/*" class="hidden" />
|
<input type="file" id="bgFileInput" accept="image/*" class="hidden" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+30
-5
@@ -68,6 +68,19 @@
|
|||||||
--board-hover-border: rgba(179,89,255,0.18);
|
--board-hover-border: rgba(179,89,255,0.18);
|
||||||
--toggle-on-bg: rgba(214,92,255,0.22);
|
--toggle-on-bg: rgba(214,92,255,0.22);
|
||||||
--logo-shadow: rgba(179,89,255,0.35);
|
--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);
|
--board-hover-border: rgba(179,89,255,0.18);
|
||||||
--toggle-on-bg: rgba(214,92,255,0.22);
|
--toggle-on-bg: rgba(214,92,255,0.22);
|
||||||
--logo-shadow: rgba(179,89,255,0.35);
|
--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"] .board { border-color: rgba(214,92,255,0.10); }
|
||||||
[data-theme="nebula"] .bm-item:hover { background: rgba(214,92,255,0.05); }
|
[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);
|
--board-hover-border: rgba(212, 189, 138, 0.20);
|
||||||
--toggle-on-bg: rgba(200,168,74,0.22);
|
--toggle-on-bg: rgba(200,168,74,0.22);
|
||||||
--logo-shadow: rgba(212, 189, 138, 0.40);
|
--logo-shadow: rgba(212, 189, 138, 0.40);
|
||||||
|
--bg-solid-fallback: #080c16;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +161,7 @@
|
|||||||
--board-hover-border: rgba(157, 92, 255, 0.22);
|
--board-hover-border: rgba(157, 92, 255, 0.22);
|
||||||
--toggle-on-bg: rgba(224,128,48,0.22);
|
--toggle-on-bg: rgba(224,128,48,0.22);
|
||||||
--logo-shadow: rgba(157, 92, 255, 0.45);
|
--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"] .board { border-color: rgba(157, 92, 255, 0.15); }
|
||||||
[data-theme="event-horizon"] .bm-item:hover { background: rgba(157, 92, 255, 0.08); }
|
[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);
|
--board-hover-border: rgba(46, 184, 184, 0.20);
|
||||||
--toggle-on-bg: rgba(78,207,207,0.22);
|
--toggle-on-bg: rgba(78,207,207,0.22);
|
||||||
--logo-shadow: rgba(46, 184, 184, 0.45);
|
--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"] .board { border-color: rgba(46, 184, 184, 0.12); }
|
||||||
[data-theme="merchantman"] .bm-item:hover { background: rgba(46, 184, 184, 0.06); }
|
[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);
|
--board-hover-border: rgba(125, 179, 255, 0.22);
|
||||||
--toggle-on-bg: rgba(91,159,255,0.22);
|
--toggle-on-bg: rgba(91,159,255,0.22);
|
||||||
--logo-shadow: rgba(125, 179, 255, 0.50);
|
--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"] .logo { font-family: 'Cinzel', serif; letter-spacing: 5px; text-transform: uppercase; }
|
||||||
[data-theme="julia-jin"] .clock { font-family: 'Cinzel', serif; font-weight: 600; }
|
[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);
|
--board-hover-border: rgba(255, 140, 61, 0.22);
|
||||||
--toggle-on-bg: rgba(240,124,48,0.22);
|
--toggle-on-bg: rgba(240,124,48,0.22);
|
||||||
--logo-shadow: rgba(255, 140, 61, 0.45);
|
--logo-shadow: rgba(255, 140, 61, 0.45);
|
||||||
|
--bg-solid-fallback: #0f0a08;
|
||||||
}
|
}
|
||||||
[data-theme="sc-sunset"] .board {
|
[data-theme="sc-sunset"] .board {
|
||||||
border-color: rgba(255, 140, 61, 0.15);
|
border-color: rgba(255, 140, 61, 0.15);
|
||||||
@@ -253,6 +272,7 @@
|
|||||||
--board-hover-border: rgba(50, 255, 106, 0.20);
|
--board-hover-border: rgba(50, 255, 106, 0.20);
|
||||||
--toggle-on-bg: rgba(34,204,68,0.20);
|
--toggle-on-bg: rgba(34,204,68,0.20);
|
||||||
--logo-shadow: rgba(50, 255, 106, 0.40);
|
--logo-shadow: rgba(50, 255, 106, 0.40);
|
||||||
|
--bg-solid-fallback: #050805;
|
||||||
--danger: #ff4d4d;
|
--danger: #ff4d4d;
|
||||||
}
|
}
|
||||||
[data-theme="hellion-hud"] .board {
|
[data-theme="hellion-hud"] .board {
|
||||||
@@ -287,6 +307,7 @@
|
|||||||
--board-hover-border: rgba(30, 255, 142, 0.25);
|
--board-hover-border: rgba(30, 255, 142, 0.25);
|
||||||
--toggle-on-bg: rgba(0,232,122,0.18);
|
--toggle-on-bg: rgba(0,232,122,0.18);
|
||||||
--logo-shadow: rgba(30, 255, 142, 0.60);
|
--logo-shadow: rgba(30, 255, 142, 0.60);
|
||||||
|
--bg-solid-fallback: #040705;
|
||||||
}
|
}
|
||||||
[data-theme="hellion-energy"] .board {
|
[data-theme="hellion-energy"] .board {
|
||||||
border-color: rgba(30, 255, 142, 0.15);
|
border-color: rgba(30, 255, 142, 0.15);
|
||||||
@@ -322,6 +343,7 @@
|
|||||||
--board-hover-border: rgba(0, 180, 216, 0.25);
|
--board-hover-border: rgba(0, 180, 216, 0.25);
|
||||||
--toggle-on-bg: rgba(0, 180, 216, 0.20);
|
--toggle-on-bg: rgba(0, 180, 216, 0.20);
|
||||||
--logo-shadow: rgba(0, 180, 216, 0.40);
|
--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"] .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); }
|
[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);
|
--board-hover-border: rgba(46, 196, 160, 0.22);
|
||||||
--toggle-on-bg: rgba(46, 196, 160, 0.18);
|
--toggle-on-bg: rgba(46, 196, 160, 0.18);
|
||||||
--logo-shadow: rgba(46, 196, 160, 0.50);
|
--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"] .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); }
|
[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);
|
--board-hover-border: rgba(94, 194, 255, 0.25);
|
||||||
--toggle-on-bg: rgba(94, 194, 255, 0.20);
|
--toggle-on-bg: rgba(94, 194, 255, 0.20);
|
||||||
--logo-shadow: rgba(94, 194, 255, 0.45);
|
--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"] .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); }
|
[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; }
|
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-local {
|
||||||
.bm-favicon-fallback {
|
width: 16px; height: 16px; flex-shrink: 0;
|
||||||
width: 14px; height: 14px; flex-shrink: 0;
|
border-radius: 3px;
|
||||||
background: var(--accent-dim); border-radius: 2px;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
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-text { flex: 1; min-width: 0; }
|
||||||
.bm-title { font-size: 12px; font-weight: 400; color: var(--text-primary); line-height: 1.3; }
|
.bm-title { font-size: 12px; font-weight: 400; color: var(--text-primary); line-height: 1.3; }
|
||||||
|
|||||||
+2
-2
@@ -105,7 +105,7 @@ async function checkBackupReminder() {
|
|||||||
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 timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : [];
|
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 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');
|
||||||
@@ -132,7 +132,7 @@ function startClock() {
|
|||||||
`${t(DAY_KEYS[now.getDay()])}, ${String(now.getDate()).padStart(2,'0')}. ${t(MONTH_KEYS[now.getMonth()])}`;
|
`${t(DAY_KEYS[now.getDay()])}, ${String(now.getDate()).padStart(2,'0')}. ${t(MONTH_KEYS[now.getMonth()])}`;
|
||||||
}
|
}
|
||||||
tick();
|
tick();
|
||||||
setInterval(tick, 1000);
|
const clockInterval = setInterval(tick, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- GLOBALE EVENTS (Header-Buttons, Modals, Import) ----
|
// ---- GLOBALE EVENTS (Header-Buttons, Modals, Import) ----
|
||||||
|
|||||||
+5
-14
@@ -215,19 +215,11 @@ function createBmEl(bm) {
|
|||||||
li.dataset.bmUrl = bm.url;
|
li.dataset.bmUrl = bm.url;
|
||||||
li.draggable = true;
|
li.draggable = true;
|
||||||
|
|
||||||
const favicon = document.createElement('img');
|
const favicon = document.createElement('div');
|
||||||
favicon.className = 'bm-favicon';
|
favicon.className = 'bm-favicon-local';
|
||||||
favicon.width = 14;
|
favicon.textContent = bm.title.charAt(0).toUpperCase();
|
||||||
favicon.height = 14;
|
const hue = (bm.title.charCodeAt(0) * 137) % 360;
|
||||||
favicon.src = getFaviconUrl(bm.url);
|
favicon.style.backgroundColor = `hsl(${hue}, 45%, 35%)`;
|
||||||
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 textDiv = document.createElement('div');
|
const textDiv = document.createElement('div');
|
||||||
textDiv.className = 'bm-text';
|
textDiv.className = 'bm-text';
|
||||||
@@ -247,7 +239,6 @@ function createBmEl(bm) {
|
|||||||
deleteBtn.textContent = '✕';
|
deleteBtn.textContent = '✕';
|
||||||
|
|
||||||
li.appendChild(favicon);
|
li.appendChild(favicon);
|
||||||
li.appendChild(fallback);
|
|
||||||
li.appendChild(textDiv);
|
li.appendChild(textDiv);
|
||||||
li.appendChild(deleteBtn);
|
li.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
|||||||
+13
-22
@@ -689,41 +689,32 @@ const Calculator = {
|
|||||||
await this.open();
|
await this.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close-Event abfangen: WidgetManager.close() ueberschreiben
|
// Widget-Lifecycle-Events
|
||||||
const origClose = WidgetManager.close.bind(WidgetManager);
|
|
||||||
const self = this;
|
const self = this;
|
||||||
WidgetManager.close = function(id) {
|
WidgetManager.on('widget:close', (e) => {
|
||||||
origClose(id);
|
if (e.detail.id === self.WIDGET_ID) {
|
||||||
if (id === self.WIDGET_ID) {
|
|
||||||
self.onClose();
|
self.onClose();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Minimize-Event abfangen
|
WidgetManager.on('widget:minimize', (e) => {
|
||||||
const origMinimize = WidgetManager.minimize.bind(WidgetManager);
|
if (e.detail.id === self.WIDGET_ID) {
|
||||||
WidgetManager.minimize = async function(id) {
|
|
||||||
await origMinimize(id);
|
|
||||||
if (id === self.WIDGET_ID) {
|
|
||||||
self._isOpen = false;
|
self._isOpen = false;
|
||||||
await self.save();
|
self.save();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Open-Event abfangen
|
WidgetManager.on('widget:open', (e) => {
|
||||||
const origOpen = WidgetManager.openWidget.bind(WidgetManager);
|
if (e.detail.id === self.WIDGET_ID) {
|
||||||
WidgetManager.openWidget = async function(id) {
|
|
||||||
await origOpen(id);
|
|
||||||
if (id === self.WIDGET_ID) {
|
|
||||||
self._isOpen = true;
|
self._isOpen = true;
|
||||||
// Body neu rendern (war durch minimize entfernt)
|
|
||||||
const body = WidgetManager.getBody(self.WIDGET_ID);
|
const body = WidgetManager.getBody(self.WIDGET_ID);
|
||||||
if (body && body.children.length === 0) {
|
if (body && body.children.length === 0) {
|
||||||
self.renderBody(body);
|
self.renderBody(body);
|
||||||
}
|
}
|
||||||
const entry = WidgetManager._widgets.get(self.WIDGET_ID);
|
const entry = WidgetManager._widgets.get(self.WIDGET_ID);
|
||||||
if (entry) self._bindKeyboard(entry.el);
|
if (entry) self._bindKeyboard(entry.el);
|
||||||
await self.save();
|
self.save();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+50
-21
@@ -9,11 +9,26 @@ function initDataButtons() {
|
|||||||
const jsonInput = document.getElementById('jsonImportInput');
|
const jsonInput = document.getElementById('jsonImportInput');
|
||||||
if (!btnExport || !btnImport) return;
|
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)
|
// Export (inkl. Notes)
|
||||||
btnExport.addEventListener('click', async () => {
|
btnExport.addEventListener('click', async () => {
|
||||||
const widgetData = await Store.get('widgetStates');
|
const widgetData = await Store.get('widgetStates');
|
||||||
const data = {
|
const data = {
|
||||||
version: '2.0.0',
|
version: '2.0.1',
|
||||||
exported: new Date().toISOString(),
|
exported: new Date().toISOString(),
|
||||||
boards,
|
boards,
|
||||||
settings,
|
settings,
|
||||||
@@ -38,18 +53,21 @@ function initDataButtons() {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(await file.text());
|
const data = JSON.parse(await file.text());
|
||||||
if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
|
if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
|
||||||
const validBoards = data.boards.filter(b => {
|
const validBoards = data.boards
|
||||||
if (!b || typeof b.title !== 'string' || !Array.isArray(b.bookmarks)) return false;
|
.filter(b => b && typeof b.title === 'string' && Array.isArray(b.bookmarks))
|
||||||
b.id = b.id || uid();
|
.map(b => ({
|
||||||
b.blurred = !!b.blurred;
|
id: b.id || uid(),
|
||||||
b.bookmarks = b.bookmarks.filter(bm => {
|
title: String(b.title).slice(0, 100),
|
||||||
if (!bm || typeof bm.title !== 'string' || typeof bm.url !== 'string') return false;
|
blurred: !!b.blurred,
|
||||||
bm.id = bm.id || uid();
|
bookmarks: b.bookmarks
|
||||||
bm.desc = bm.desc || '';
|
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
||||||
return true;
|
.map(bm => ({
|
||||||
});
|
id: bm.id || uid(),
|
||||||
return true;
|
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'));
|
if (validBoards.length === 0) throw new Error(t('data.no_boards'));
|
||||||
const ok = await HellionDialog.confirm(
|
const ok = await HellionDialog.confirm(
|
||||||
t('data.import_confirm', { count: validBoards.length }),
|
t('data.import_confirm', { count: validBoards.length }),
|
||||||
@@ -65,18 +83,26 @@ function initDataButtons() {
|
|||||||
const existingWidgets = await Store.get('widgetStates') || {};
|
const existingWidgets = await Store.get('widgetStates') || {};
|
||||||
if (Array.isArray(data.notes) && data.notes.length > 0) {
|
if (Array.isArray(data.notes) && data.notes.length > 0) {
|
||||||
const existingNotes = Array.isArray(existingWidgets.notes) ? existingWidgets.notes : [];
|
const existingNotes = Array.isArray(existingWidgets.notes) ? existingWidgets.notes : [];
|
||||||
const importNotes = data.notes.filter(n => {
|
const importNotes = data.notes
|
||||||
if (!n || !n.id || !n.template) return false;
|
.filter(n => n && n.id && n.template)
|
||||||
n.checklistItems = Array.isArray(n.checklistItems) ? n.checklistItems : [];
|
.map(n => ({
|
||||||
return true;
|
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
|
// Limit beachten
|
||||||
const spaceLeft = Notes.MAX_NOTES - existingNotes.length;
|
const spaceLeft = Notes.MAX_NOTES - existingNotes.length;
|
||||||
const toImport = importNotes.slice(0, spaceLeft);
|
const toImport = importNotes.slice(0, spaceLeft);
|
||||||
if (toImport.length > 0) {
|
if (toImport.length > 0) {
|
||||||
const merged = [...existingNotes, ...toImport];
|
const merged = [...existingNotes, ...toImport];
|
||||||
existingWidgets.notes = merged;
|
existingWidgets.notes = merged;
|
||||||
Notes._notes = merged;
|
|
||||||
notesImported = toImport.length;
|
notesImported = toImport.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +116,6 @@ function initDataButtons() {
|
|||||||
existingWidgets.calculator = { x: 400, y: 120, width: 280, height: 400, open: false, history: [] };
|
existingWidgets.calculator = { x: 400, y: 120, width: 280, height: 400, open: false, history: [] };
|
||||||
}
|
}
|
||||||
existingWidgets.calculator.history = calcHistory.slice(0, Calculator.MAX_HISTORY);
|
existingWidgets.calculator.history = calcHistory.slice(0, Calculator.MAX_HISTORY);
|
||||||
Calculator._history = existingWidgets.calculator.history;
|
|
||||||
calcImported = true;
|
calcImported = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +129,6 @@ function initDataButtons() {
|
|||||||
existingWidgets.timer = { x: 600, y: 80, width: 260, height: 360, open: false, presets: [] };
|
existingWidgets.timer = { x: 600, y: 80, width: 260, height: 360, open: false, presets: [] };
|
||||||
}
|
}
|
||||||
existingWidgets.timer.presets = validPresets.slice(0, Timer.MAX_PRESETS);
|
existingWidgets.timer.presets = validPresets.slice(0, Timer.MAX_PRESETS);
|
||||||
Timer._presets = existingWidgets.timer.presets;
|
|
||||||
timerImported = true;
|
timerImported = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,6 +136,11 @@ function initDataButtons() {
|
|||||||
// Gemeinsam speichern
|
// Gemeinsam speichern
|
||||||
await Store.set('widgetStates', existingWidgets);
|
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 noteMsg = notesImported > 0 ? t('data.notes_suffix', { count: notesImported }) : '';
|
||||||
const calcMsg = calcImported ? t('data.calc_suffix') : '';
|
const calcMsg = calcImported ? t('data.calc_suffix') : '';
|
||||||
const timerMsg = timerImported ? t('data.timer_suffix') : '';
|
const timerMsg = timerImported ? t('data.timer_suffix') : '';
|
||||||
|
|||||||
@@ -202,6 +202,13 @@ const STRINGS = {
|
|||||||
'header.theme': 'Darstellung',
|
'header.theme': 'Darstellung',
|
||||||
'header.settings': 'Einstellungen',
|
'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-Panel Überschrift
|
||||||
'settings.title': 'Einstellungen',
|
'settings.title': 'Einstellungen',
|
||||||
|
|
||||||
@@ -255,6 +262,13 @@ const STRINGS = {
|
|||||||
'settings.bg_upload.desc': 'Lokales Bild als Hintergrund verwenden',
|
'settings.bg_upload.desc': 'Lokales Bild als Hintergrund verwenden',
|
||||||
'settings.search_engine_toggle': 'Suchmaschine wechseln',
|
'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
|
// Modals
|
||||||
'modal.new_board': 'Neues Board',
|
'modal.new_board': 'Neues Board',
|
||||||
'modal.board_name': 'Board-Name...',
|
'modal.board_name': 'Board-Name...',
|
||||||
@@ -493,6 +507,13 @@ const STRINGS = {
|
|||||||
'header.theme': 'Theme',
|
'header.theme': 'Theme',
|
||||||
'header.settings': 'Settings',
|
'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 panel heading
|
||||||
'settings.title': 'Settings',
|
'settings.title': 'Settings',
|
||||||
|
|
||||||
@@ -546,6 +567,13 @@ const STRINGS = {
|
|||||||
'settings.bg_upload.desc': 'Use a local image as background',
|
'settings.bg_upload.desc': 'Use a local image as background',
|
||||||
'settings.search_engine_toggle': 'Switch search engine',
|
'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
|
// Modals
|
||||||
'modal.new_board': 'New Board',
|
'modal.new_board': 'New Board',
|
||||||
'modal.board_name': 'Board name...',
|
'modal.board_name': 'Board name...',
|
||||||
|
|||||||
+16
-25
@@ -460,41 +460,32 @@ const ImageRef = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close-Event abfangen
|
// Widget-Lifecycle-Events
|
||||||
const self = this;
|
const self = this;
|
||||||
const prevClose = WidgetManager.close;
|
WidgetManager.on('widget:close', (e) => {
|
||||||
WidgetManager.close = function(id) {
|
const isImage = self._images.some(img => img.id === e.detail.id);
|
||||||
prevClose.call(WidgetManager, id);
|
|
||||||
// Pruefen ob es ein Image-Widget ist
|
|
||||||
const isImage = self._images.some(img => img.id === id);
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
self.onClose(id);
|
self.onClose(e.detail.id);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Minimize-Event abfangen
|
WidgetManager.on('widget:minimize', (e) => {
|
||||||
const prevMinimize = WidgetManager.minimize;
|
const isImage = self._images.some(img => img.id === e.detail.id);
|
||||||
WidgetManager.minimize = async function(id) {
|
|
||||||
await prevMinimize.call(WidgetManager, id);
|
|
||||||
const isImage = self._images.some(img => img.id === id);
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
await self.save();
|
self.save();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Open-Event abfangen
|
WidgetManager.on('widget:open', (e) => {
|
||||||
const prevOpen = WidgetManager.openWidget;
|
const imgData = self._images.find(img => img.id === e.detail.id);
|
||||||
WidgetManager.openWidget = async function(id) {
|
|
||||||
await prevOpen.call(WidgetManager, id);
|
|
||||||
const imgData = self._images.find(img => img.id === id);
|
|
||||||
if (imgData) {
|
if (imgData) {
|
||||||
const body = WidgetManager.getBody(id);
|
const body = WidgetManager.getBody(e.detail.id);
|
||||||
if (body && body.children.length === 0) {
|
if (body && body.children.length === 0) {
|
||||||
const dataUrl = self._getSessionImage(id);
|
const dataUrl = self._getSessionImage(e.detail.id);
|
||||||
self.renderBody(imgData, body, dataUrl);
|
self.renderBody(imgData, body, dataUrl);
|
||||||
}
|
}
|
||||||
await self.save();
|
self.save();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+19
-1
@@ -23,6 +23,17 @@ function closeThemeModal() {
|
|||||||
overlay.classList.remove('active');
|
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 ----
|
// ---- ACCORDION ----
|
||||||
function initAccordion() {
|
function initAccordion() {
|
||||||
const defaultOpen = new Set(['widgets']);
|
const defaultOpen = new Set(['widgets']);
|
||||||
@@ -89,8 +100,10 @@ function applySettings() {
|
|||||||
|
|
||||||
applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
|
applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
|
||||||
|
|
||||||
if (settings.bgUrl) {
|
if (settings.bgUrl && isValidBgUrl(settings.bgUrl)) {
|
||||||
document.getElementById('bgLayer').style.backgroundImage = `url('${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 () => {
|
document.getElementById('btnApplyBg').addEventListener('click', async () => {
|
||||||
const url = document.getElementById('bgUrlInput').value.trim();
|
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;
|
settings.bgUrl = url;
|
||||||
document.getElementById('bgLayer').style.backgroundImage = url ? `url('${url}')` : '';
|
document.getElementById('bgLayer').style.backgroundImage = url ? `url('${url}')` : '';
|
||||||
await saveSettings();
|
await saveSettings();
|
||||||
@@ -183,6 +200,7 @@ function bindSettingsEvents() {
|
|||||||
if (!file) return;
|
if (!file) return;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async ev => {
|
reader.onload = async ev => {
|
||||||
|
if (!isValidBgUrl(ev.target.result)) return;
|
||||||
settings.bgUrl = ev.target.result;
|
settings.bgUrl = ev.target.result;
|
||||||
document.getElementById('bgLayer').style.backgroundImage = `url('${ev.target.result}')`;
|
document.getElementById('bgLayer').style.backgroundImage = `url('${ev.target.result}')`;
|
||||||
await saveSettings();
|
await saveSettings();
|
||||||
|
|||||||
@@ -33,15 +33,6 @@ function escHtml(str) {
|
|||||||
.replace(/"/g, '"');
|
.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() {
|
function getDefaultBoards() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
+13
-22
@@ -720,32 +720,23 @@ const Timer = {
|
|||||||
await this.open();
|
await this.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close-Event abfangen
|
// Widget-Lifecycle-Events
|
||||||
const origClose = WidgetManager.close.bind(WidgetManager);
|
|
||||||
const self = this;
|
const self = this;
|
||||||
const prevClose = WidgetManager.close;
|
WidgetManager.on('widget:close', (e) => {
|
||||||
WidgetManager.close = function(id) {
|
if (e.detail.id === self.WIDGET_ID) {
|
||||||
prevClose.call(WidgetManager, id);
|
|
||||||
if (id === self.WIDGET_ID) {
|
|
||||||
self.onClose();
|
self.onClose();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Minimize-Event abfangen
|
WidgetManager.on('widget:minimize', (e) => {
|
||||||
const prevMinimize = WidgetManager.minimize;
|
if (e.detail.id === self.WIDGET_ID) {
|
||||||
WidgetManager.minimize = async function(id) {
|
|
||||||
await prevMinimize.call(WidgetManager, id);
|
|
||||||
if (id === self.WIDGET_ID) {
|
|
||||||
self._isOpen = false;
|
self._isOpen = false;
|
||||||
await self.save();
|
self.save();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Open-Event abfangen
|
WidgetManager.on('widget:open', (e) => {
|
||||||
const prevOpen = WidgetManager.openWidget;
|
if (e.detail.id === self.WIDGET_ID) {
|
||||||
WidgetManager.openWidget = async function(id) {
|
|
||||||
await prevOpen.call(WidgetManager, id);
|
|
||||||
if (id === self.WIDGET_ID) {
|
|
||||||
self._isOpen = true;
|
self._isOpen = true;
|
||||||
const body = WidgetManager.getBody(self.WIDGET_ID);
|
const body = WidgetManager.getBody(self.WIDGET_ID);
|
||||||
if (body && body.children.length === 0) {
|
if (body && body.children.length === 0) {
|
||||||
@@ -753,8 +744,8 @@ const Timer = {
|
|||||||
}
|
}
|
||||||
const entry = WidgetManager._widgets.get(self.WIDGET_ID);
|
const entry = WidgetManager._widgets.get(self.WIDGET_ID);
|
||||||
if (entry) self._bindKeyboard(entry.el);
|
if (entry) self._bindKeyboard(entry.el);
|
||||||
await self.save();
|
self.save();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+52
-5
@@ -9,6 +9,27 @@ const WidgetManager = {
|
|||||||
_topZ: 100,
|
_topZ: 100,
|
||||||
STORAGE_KEY: 'widgetStates',
|
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
|
* Widget erstellen und in DOM einfuegen
|
||||||
* @param {string} type - 'note'
|
* @param {string} type - 'note'
|
||||||
@@ -31,7 +52,7 @@ const WidgetManager = {
|
|||||||
const el = this._buildDOM(state);
|
const el = this._buildDOM(state);
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
|
|
||||||
this._widgets.set(id, { el, type, state });
|
this._widgets.set(id, { el, type, state, _minimizing: false });
|
||||||
this._initDrag(el);
|
this._initDrag(el);
|
||||||
this._initResize(el);
|
this._initResize(el);
|
||||||
this.bringToFront(id);
|
this.bringToFront(id);
|
||||||
@@ -144,22 +165,47 @@ const WidgetManager = {
|
|||||||
const entry = this._widgets.get(id);
|
const entry = this._widgets.get(id);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
entry.el.remove();
|
entry.el.remove();
|
||||||
|
this._emitter.dispatchEvent(new CustomEvent('widget:close', { detail: { id } }));
|
||||||
this._widgets.delete(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
|
* @param {string} id
|
||||||
*/
|
*/
|
||||||
async minimize(id) {
|
async minimize(id) {
|
||||||
const entry = this._widgets.get(id);
|
const entry = this._widgets.get(id);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
entry.state.open = false;
|
entry.state.open = false;
|
||||||
|
entry._minimizing = true;
|
||||||
entry.el.classList.add('widget-minimized');
|
entry.el.classList.add('widget-minimized');
|
||||||
setTimeout(() => {
|
|
||||||
|
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.el.style.display = 'none';
|
||||||
}, 250);
|
}
|
||||||
|
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();
|
await this.save();
|
||||||
|
this._emitter.dispatchEvent(new CustomEvent('widget:minimize', { detail: { id } }));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,14 +215,15 @@ const WidgetManager = {
|
|||||||
async openWidget(id) {
|
async openWidget(id) {
|
||||||
const entry = this._widgets.get(id);
|
const entry = this._widgets.get(id);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
|
entry._minimizing = false;
|
||||||
entry.state.open = true;
|
entry.state.open = true;
|
||||||
entry.el.style.display = 'flex';
|
entry.el.style.display = 'flex';
|
||||||
// Naechster Frame fuer Animation
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
entry.el.classList.remove('widget-minimized');
|
entry.el.classList.remove('widget-minimized');
|
||||||
});
|
});
|
||||||
this.bringToFront(id);
|
this.bringToFront(id);
|
||||||
await this.save();
|
await this.save();
|
||||||
|
this._emitter.dispatchEvent(new CustomEvent('widget:open', { detail: { id } }));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user