15 Commits

Author SHA1 Message Date
JonKazama-Hellion c6c0d5c468 docs(release): architecture.md auf v2.0.0 aktualisieren
i18n.js und bookmark-import.js ergänzen, Load-Order und
Module-Tabelle an aktuelle newtab.html synchronisieren.
2026-03-22 18:38:18 +01:00
JonKazama-Hellion dbd209bc2b docs(release): README auf v2.0.0 mit i18n-Feature aktualisieren
Version, i18n-Sektion, Architecture-Tree mit i18n.js und _locales,
Modulanzahl und Release-Beispiel aktualisiert.
2026-03-22 18:33:27 +01:00
JonKazama-Hellion 7900962c5a fix(i18n): Review-Findings beheben
- Version in newtab.html auf 2.0.0 aktualisieren
- dialog.js OK-Fallback auf t('dialog.ok') umstellen
- Duplikat-Keys widgets.minimize/close entfernen (widget.* wird genutzt)
2026-03-22 18:28:35 +01:00
JonKazama-Hellion 1bbdbdef1c feat(release): Version auf v2.0.0 bumpen — i18n Release
Alle drei Manifests, Export-Versionen und CHANGELOG aktualisiert.
2026-03-22 18:28:35 +01:00
JonKazama-Hellion f07200cd8e feat(i18n): settings.js und widgets.js auf t() umstellen
Reset-Dialog, Dateifehler, Widget-Buttons (Minimieren/Schließen)
und Default-Titel verwenden jetzt i18n-Keys.
2026-03-22 18:28:35 +01:00
JonKazama-Hellion ab165d4f75 feat(i18n): app.js Strings auf t() umstellen
Clock-Tage/Monate, Backup-Reminder, HTML-Import und
URL-Validierung verwenden jetzt i18n-Keys.
2026-03-22 18:28:35 +01:00
JonKazama-Hellion 4a66015258 feat(i18n): data.js, bookmark-import.js, storage.js auf t() umstellen 2026-03-22 18:28:35 +01:00
JonKazama-Hellion d0f870ace1 feat(i18n): calculator.js, timer.js, image-ref.js auf t() umstellen 2026-03-22 18:28:35 +01:00
JonKazama-Hellion daea57a9df feat(i18n): notes.js Strings auf t() umstellen 2026-03-22 18:28:35 +01:00
JonKazama-Hellion f937f7c39c feat(i18n): onboarding.js Strings auf t() umstellen 2026-03-22 18:28:31 +01:00
JonKazama-Hellion 3ab8847f31 feat(i18n): boards.js Strings auf t() umstellen 2026-03-22 18:28:25 +01:00
JonKazama-Hellion 36335d3cc4 feat(i18n): dialog.js Defaults auf t() umstellen 2026-03-22 18:28:20 +01:00
JonKazama-Hellion 1b39ac863b fix(i18n): Code-Quality-Review Korrekturen
- resolveLang() Hilfsfunktion extrahieren (DRY)
- JSDoc-Kommentar in I18n.init() korrigieren
- settings.export.btn Key für Export-Button trennen
- setLanguage('auto') im Reset-Handler aufrufen
2026-03-22 14:07:17 +01:00
JonKazama-Hellion 522b177470 fix(i18n): Spec-Review-Korrekturen für i18n-Grundgerüst
- header.settings DE-String korrigiert (war englisch)
- ~17 deutsche Strings die englisch waren gefixt
- I18n.init() vor applySettings() in app.js
- html lang-Attribut dynamisch via I18n setzen
- Default html lang auf en (passend zu default_locale)
2026-03-22 14:04:04 +01:00
JonKazama-Hellion f2d4e22b86 feat(i18n): i18n-Modul, _locales und data-i18n Attribute einrichten 2026-03-22 13:56:04 +01:00
24 changed files with 1019 additions and 274 deletions
+21
View File
@@ -6,6 +6,27 @@ All notable changes per version. Format based on [Keep a Changelog](https://keep
--- ---
### v2.0.0 — 22.03.2026
#### New Features
- **Internationalization (i18n)** — Full DE/EN language support with runtime switching
- Language setting in Settings panel: German, English or Auto-detect (browser language)
- `i18n.js` module with ~220+ string keys, `t(key, vars?)` helper and `data-i18n` HTML attributes
- `_locales/de/` and `_locales/en/` for manifest-level i18n (`__MSG_extName__`, `__MSG_extDesc__`)
- `<html lang>` attribute updates dynamically when language changes
- All modules migrated: dialog, boards, onboarding, notes, calculator, timer, image-ref, data, bookmark-import, storage, settings, widgets, app
#### Technical
- New script load order: `storage → state → i18n → dialog → ...`
- `applyLanguage()` scans DOM for `data-i18n`, `data-i18n-placeholder`, `data-i18n-title`
- Onboarding slides use i18n keys instead of hardcoded text (rendered at display time)
- Clock day/month names via i18n keys instead of hardcoded arrays
- `resolveLang()` helper for DRY language resolution (auto → browser detect)
---
### v1.10.0 — 22.03.2026 ### v1.10.0 — 22.03.2026
#### Themes #### Themes
+19 -7
View File
@@ -1,6 +1,6 @@
# ⬡ Hellion Dashboard v1.9.0 # ⬡ Hellion Dashboard v2.0.0
![Version](https://img.shields.io/badge/Version-1.9.0-blue) ![Version](https://img.shields.io/badge/Version-2.0.0-blue)
![JavaScript](https://img.shields.io/badge/JavaScript-Vanilla%20ES2020-F7DF1E?logo=javascript&logoColor=black) ![JavaScript](https://img.shields.io/badge/JavaScript-Vanilla%20ES2020-F7DF1E?logo=javascript&logoColor=black)
![Manifest](https://img.shields.io/badge/Manifest-V3-green) ![Manifest](https://img.shields.io/badge/Manifest-V3-green)
![License](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-orange) ![License](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-orange)
@@ -10,7 +10,8 @@
**No account. No subscription. No cloud. All data stays 100% local.** **No account. No subscription. No cloud. All data stays 100% local.**
A personal bookmark dashboard as a browser extension. A personal bookmark dashboard as a browser extension.
Boards, drag & drop, 11 themes, search bar, widget system with notes, calculator, timer and more. All in the browser, all offline. Boards, drag & drop, 11 themes, search bar, widget system with notes, calculator, timer and more.
Full DE/EN language support with runtime switching. All in the browser, all offline.
No external data transmission, no trackers, no analytics, no ads. No external data transmission, no trackers, no analytics, no ads.
Developed by **[Hellion Online Media — Florian Wathling](https://hellion-media.de)** — JonKazama-Hellion. Developed by **[Hellion Online Media — Florian Wathling](https://hellion-media.de)** — JonKazama-Hellion.
@@ -89,6 +90,12 @@ What you see is what's saved. No magic.
| Avorion | Own work, screenshot from Avorion, Hellion Initiative ship | Hellion Online Media | | Avorion | Own work, screenshot from Avorion, Hellion Initiative ship | Hellion Online Media |
| Hellion Stealth | Screenshot from Star Citizen by Cloud Imperium Games | Fan Content | | Hellion Stealth | Screenshot from Star Citizen by Cloud Imperium Games | Fan Content |
### Language Support (i18n)
- German and English with runtime switching via Settings
- Auto-detect from browser language, manual override available
- All UI elements, dialogs, onboarding and widget labels fully translated
### Onboarding & Dialogs ### Onboarding & Dialogs
- 7-step welcome flow on first launch with widget explanation and optional gaming starter board - 7-step welcome flow on first launch with widget explanation and optional gaming starter board
@@ -103,7 +110,7 @@ What you see is what's saved. No magic.
- Compact mode, shorten titles, search bar toggle, open links in new tab, descriptions, hide extra bookmarks - Compact mode, shorten titles, search bar toggle, open links in new tab, descriptions, hide extra bookmarks
- JSON export & import (backup & restore) - JSON export & import (backup & restore)
- Onboarding repeatable - Onboarding repeatable
- All UI labels in German (English coming in v2.1) - Language setting: German, English or auto-detect
--- ---
@@ -223,10 +230,15 @@ hellion-newtab/
├── SECURITY.md # Security policy and reporting ├── SECURITY.md # Security policy and reporting
├── DISCLAIMER.md # Disclaimer and legal ├── DISCLAIMER.md # Disclaimer and legal
├── _locales/
│ ├── de/messages.json # Manifest-level i18n (German)
│ └── en/messages.json # Manifest-level i18n (English)
├── src/ ├── src/
│ ├── js/ │ ├── js/
│ │ ├── storage.js # Storage abstraction + quota check │ │ ├── storage.js # Storage abstraction + quota check
│ │ ├── state.js # Global state, defaults, helpers │ │ ├── state.js # Global state, defaults, helpers
│ │ ├── i18n.js # Internationalization (DE/EN, ~220+ keys, t() helper)
│ │ ├── dialog.js # Custom dialog system (HellionDialog.alert/confirm) │ │ ├── dialog.js # Custom dialog system (HellionDialog.alert/confirm)
│ │ ├── themes.js # Theme definitions & application (11 themes) │ │ ├── themes.js # Theme definitions & application (11 themes)
│ │ ├── boards.js # Board/bookmark rendering, event delegation, modals │ │ ├── boards.js # Board/bookmark rendering, event delegation, modals
@@ -272,7 +284,7 @@ hellion-newtab/
- **Zero Dependencies** — No npm, no build, no framework. Runs directly - **Zero Dependencies** — No npm, no build, no framework. Runs directly
- **Privacy First** — All data local, no server contact - **Privacy First** — All data local, no server contact
- **Modular** — 15 JS files with clear responsibilities - **Modular** — 16 JS files with clear responsibilities
- **Responsive** — Tablet (768px) and smartphone (480px) breakpoints - **Responsive** — Tablet (768px) and smartphone (480px) breakpoints
- **Secure** — `createElement` instead of `innerHTML`, URL validation, storage error handling - **Secure** — `createElement` instead of `innerHTML`, URL validation, storage error handling
- **Event Delegation** — One listener per board list instead of per bookmark (performance) - **Event Delegation** — One listener per board list instead of per bookmark (performance)
@@ -305,8 +317,8 @@ hellion-newtab/
```bash ```bash
# Create a release: # Create a release:
git tag v1.10.0 git tag v2.0.0
git push origin v1.10.0 git push origin v2.0.0
# → GitHub Action automatically creates release with ZIP files # → GitHub Action automatically creates release with ZIP files
``` ```
+4
View File
@@ -0,0 +1,4 @@
{
"extName": { "message": "Hellion NewTab" },
"extDesc": { "message": "Persönliches Bookmark-Dashboard mit Boards, Widgets und 11 Themes. Komplett lokal, keine Cloud, kein Tracking." }
}
+4
View File
@@ -0,0 +1,4 @@
{
"extName": { "message": "Hellion NewTab" },
"extDesc": { "message": "Personal bookmark dashboard with boards, widgets, and 11 themes. Local-only, no cloud, no tracking." }
}
+14 -8
View File
@@ -27,21 +27,23 @@ HOM_NewTab_Project/
│ ├── css/ │ ├── css/
│ │ └── main.css # All styles, 11 themes, responsive breakpoints │ │ └── main.css # All styles, 11 themes, responsive breakpoints
│ └── js/ │ └── js/
│ ├── dialog.js # Custom dialog system (alert, confirm)
│ ├── storage.js # Storage abstraction layer │ ├── storage.js # Storage abstraction layer
│ ├── state.js # Global state, defaults, helpers │ ├── state.js # Global state, defaults, helpers
│ ├── i18n.js # Internationalization (DE/EN, t() helper)
│ ├── dialog.js # Custom dialog system (alert, confirm)
│ ├── themes.js # Theme definitions & application (11 themes) │ ├── themes.js # Theme definitions & application (11 themes)
│ ├── boards.js # Board/bookmark rendering & events
│ ├── drag.js # Drag & drop (Pointer Events API) │ ├── drag.js # Drag & drop (Pointer Events API)
│ ├── boards.js # Board/bookmark rendering & events
│ ├── settings.js # Settings panel, toggles, theme picker │ ├── settings.js # Settings panel, toggles, theme picker
│ ├── search.js # Search bar (Google, DuckDuckGo, Bing) │ ├── search.js # Search bar (Google, DuckDuckGo, Bing)
│ ├── onboarding.js # First-run onboarding flow
│ ├── widgets.js # Widget manager (registry, drag, resize) │ ├── widgets.js # Widget manager (registry, drag, resize)
│ ├── notes.js # Notes/checklists (multi-instance widgets) │ ├── notes.js # Notes/checklists (multi-instance widgets)
│ ├── calculator.js # Calculator widget (single-instance) │ ├── calculator.js # Calculator widget (single-instance)
│ ├── timer.js # Timer/countdown widget (single-instance) │ ├── timer.js # Timer/countdown widget (single-instance)
│ ├── image-ref.js # Image reference widget (multi-instance) │ ├── image-ref.js # Image reference widget (multi-instance)
│ ├── bookmark-import.js # Browser bookmark import (chrome.bookmarks API)
│ ├── data.js # JSON export/import (backup & restore) │ ├── data.js # JSON export/import (backup & restore)
│ ├── onboarding.js # First-run onboarding flow
│ └── app.js # Init, clock, global events (entry point) │ └── app.js # Init, clock, global events (entry point)
├── assets/ ├── assets/
│ ├── fonts/ # Local fonts (Rajdhani, Inter, Cinzel) │ ├── fonts/ # Local fonts (Rajdhani, Inter, Cinzel)
@@ -58,21 +60,23 @@ Each module has exactly one responsibility. Communication happens through global
| Module | Responsibility | | Module | Responsibility |
|---|---| |---|---|
| `dialog.js` | `HellionDialog.alert()` and `HellionDialog.confirm()` — custom styled dialogs that replace native browser popups. Loaded first so every other module can use it. |
| `storage.js` | The **only** place that touches `chrome.storage` / `localStorage`. Everything else goes through `Store.get()` / `Store.set()`. | | `storage.js` | The **only** place that touches `chrome.storage` / `localStorage`. Everything else goes through `Store.get()` / `Store.set()`. |
| `state.js` | Global `boards` and `settings` arrays, default values, `uid()`, `escHtml()`, `getFaviconUrl()`. | | `state.js` | Global `boards` and `settings` arrays, default values, `uid()`, `escHtml()`, `getFaviconUrl()`. |
| `i18n.js` | Internationalization module. `STRINGS` object with ~220+ keys (DE/EN), `t(key, vars?)` helper, `applyLanguage()` DOM scanner, `setLanguage()`, `I18n.init()`. |
| `dialog.js` | `HellionDialog.alert()` and `HellionDialog.confirm()` — custom styled dialogs that replace native browser popups. |
| `themes.js` | Applies theme CSS variables. 11 themes, each with its own `[data-theme]` block in `main.css`. | | `themes.js` | Applies theme CSS variables. 11 themes, each with its own `[data-theme]` block in `main.css`. |
| `boards.js` | Renders boards and bookmarks. Event delegation on board containers. | | `boards.js` | Renders boards and bookmarks. Event delegation on board containers. |
| `drag.js` | Board and bookmark reordering via Pointer Events API. | | `drag.js` | Board and bookmark reordering via Pointer Events API. |
| `settings.js` | Settings panel UI, toggle handlers, appearance modal, background upload. | | `settings.js` | Settings panel UI, toggle handlers, appearance modal, background upload. |
| `search.js` | Search bar with engine switching (Google, DuckDuckGo, Bing). | | `search.js` | Search bar with engine switching (Google, DuckDuckGo, Bing). |
| `onboarding.js` | Multi-slide first-run flow including the gaming starter board opt-in. |
| `widgets.js` | Widget manager — creates DOM, handles drag/resize/z-index, provides registry. See [widget-schema.md](widget-schema.md). | | `widgets.js` | Widget manager — creates DOM, handles drag/resize/z-index, provides registry. See [widget-schema.md](widget-schema.md). |
| `notes.js` | Notes and checklists as widgets. Multi-instance (max 5). Notebook sidebar. Also handles widget toolbar events. | | `notes.js` | Notes and checklists as widgets. Multi-instance (max 5). Notebook sidebar. Also handles widget toolbar events. |
| `calculator.js` | Calculator widget. Single-instance. Shunting-yard expression parser — no `eval()`. | | `calculator.js` | Calculator widget. Single-instance. Shunting-yard expression parser — no `eval()`. |
| `timer.js` | Timer/countdown widget. Single-instance. Presets, Web Audio API alarm, tab-title blink on completion. | | `timer.js` | Timer/countdown widget. Single-instance. Presets, Web Audio API alarm, tab-title blink on completion. |
| `image-ref.js` | Image reference widget. Multi-instance (max 3). Canvas API WebP conversion, sessionStorage for image data — cleared on browser close. | | `image-ref.js` | Image reference widget. Multi-instance (max 3). Canvas API WebP conversion, sessionStorage for image data — cleared on browser close. |
| `bookmark-import.js` | Direct browser bookmark import via `chrome.bookmarks.getTree()`. Folder selection modal with duplicate detection. |
| `data.js` | JSON export/import with validation. Covers boards, notes, calculator history and timer presets. | | `data.js` | JSON export/import with validation. Covers boards, notes, calculator history and timer presets. |
| `onboarding.js` | Multi-slide first-run flow including the gaming starter board opt-in. |
| `app.js` | Entry point. Calls `init()` on DOMContentLoaded. Clock, global event binding. | | `app.js` | Entry point. Calls `init()` on DOMContentLoaded. Clock, global event binding. |
--- ---
@@ -106,21 +110,23 @@ DOMContentLoaded
Scripts are loaded in `newtab.html` in dependency order. A module may only reference modules loaded before it — there is no bundler to handle this automatically. Scripts are loaded in `newtab.html` in dependency order. A module may only reference modules loaded before it — there is no bundler to handle this automatically.
```html ```html
<script src="src/js/dialog.js"></script>
<script src="src/js/storage.js"></script> <script src="src/js/storage.js"></script>
<script src="src/js/state.js"></script> <script src="src/js/state.js"></script>
<script src="src/js/i18n.js"></script>
<script src="src/js/dialog.js"></script>
<script src="src/js/themes.js"></script> <script src="src/js/themes.js"></script>
<script src="src/js/boards.js"></script>
<script src="src/js/drag.js"></script> <script src="src/js/drag.js"></script>
<script src="src/js/boards.js"></script>
<script src="src/js/settings.js"></script> <script src="src/js/settings.js"></script>
<script src="src/js/search.js"></script> <script src="src/js/search.js"></script>
<script src="src/js/onboarding.js"></script>
<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/timer.js"></script>
<script src="src/js/image-ref.js"></script> <script src="src/js/image-ref.js"></script>
<script src="src/js/bookmark-import.js"></script>
<script src="src/js/data.js"></script> <script src="src/js/data.js"></script>
<script src="src/js/onboarding.js"></script>
<script src="src/js/app.js"></script> <script src="src/js/app.js"></script>
``` ```
+4 -3
View File
@@ -1,8 +1,9 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Hellion NewTab", "name": "__MSG_extName__",
"version": "1.11.1", "default_locale": "en",
"description": "Personal bookmark dashboard — local, private, no account needed. By Hellion Online Media.", "version": "2.0.0",
"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",
+4 -3
View File
@@ -1,8 +1,9 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Hellion NewTab", "name": "__MSG_extName__",
"version": "1.11.1", "default_locale": "en",
"description": "Personal bookmark dashboard — local, private, no account needed. By Hellion Online Media.", "version": "2.0.0",
"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",
"chrome_url_overrides": { "chrome_url_overrides": {
+4 -3
View File
@@ -1,8 +1,9 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Hellion Dashboard (GX Native)", "name": "__MSG_extName__",
"version": "1.11.1", "default_locale": "en",
"description": "Ersetzt die Opera GX Startseite durch dein persönliches, leistungsoptimiertes Hellion Dashboard. Schnell, sauber und werbefrei.", "version": "2.0.0",
"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",
+105 -82
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -25,23 +25,23 @@
<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)">
<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>
Import <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">
<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>
Board <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">
<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>
Note <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">
<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>
Darstellung <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">
<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>
Settings <span data-i18n="header.settings">Settings</span>
</button> </button>
</div> </div>
</header> </header>
@@ -49,10 +49,10 @@
<!-- SEARCH BAR --> <!-- SEARCH BAR -->
<div class="search-bar-wrapper" id="searchBarWrapper"> <div class="search-bar-wrapper" id="searchBarWrapper">
<div class="search-bar"> <div class="search-bar">
<button class="search-engine-toggle" id="searchEngineToggle" title="Suchmaschine wechseln"> <button class="search-engine-toggle" id="searchEngineToggle" data-i18n-title="settings.search_engine_toggle" title="Suchmaschine wechseln">
<span id="searchEngineIcon">G</span> <span id="searchEngineIcon">G</span>
</button> </button>
<input type="text" class="search-input" id="searchInput" placeholder="Search the web…" autocomplete="off" /> <input type="text" class="search-input" id="searchInput" data-i18n-placeholder="search.placeholder" placeholder="Search the web…" autocomplete="off" />
<button class="search-submit" id="searchSubmit"> <button class="search-submit" id="searchSubmit">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</button> </button>
@@ -61,22 +61,22 @@
<!-- WIDGET TOOLBAR --> <!-- WIDGET TOOLBAR -->
<div class="widget-toolbar" id="widgetToolbar"> <div class="widget-toolbar" id="widgetToolbar">
<button class="widget-toolbar-btn" data-action="new-note" title="Note erstellen"> <button class="widget-toolbar-btn" data-action="new-note" data-i18n-title="toolbar.note" title="Note erstellen">
<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>
</button> </button>
<button class="widget-toolbar-btn" data-action="new-checklist" title="Checkliste erstellen"> <button class="widget-toolbar-btn" data-action="new-checklist" data-i18n-title="toolbar.checklist" title="Checkliste erstellen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
</button> </button>
<button class="widget-toolbar-btn" data-action="calculator" title="Taschenrechner"> <button class="widget-toolbar-btn" data-action="calculator" data-i18n-title="toolbar.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"> <button class="widget-toolbar-btn" data-action="timer" data-i18n-title="toolbar.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> <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>
<button class="widget-toolbar-btn hidden" data-action="image-ref" title="Bild-Referenz"> <button class="widget-toolbar-btn hidden" data-action="image-ref" data-i18n-title="toolbar.imageref" title="Bild-Referenz">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
</button> </button>
<button class="widget-toolbar-btn" data-action="notebook" title="Alle Notes"> <button class="widget-toolbar-btn" data-action="notebook" data-i18n-title="toolbar.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>
</div> </div>
@@ -85,7 +85,7 @@
<div class="notebook-overlay" id="notebookOverlay"></div> <div class="notebook-overlay" id="notebookOverlay"></div>
<aside class="notebook-panel" id="notebookPanel"> <aside class="notebook-panel" id="notebookPanel">
<div class="notebook-header"> <div class="notebook-header">
<span class="notebook-header-title">Notebook <span class="notebook-count" id="notebookCount">0 / 5</span></span> <span class="notebook-header-title"><span data-i18n="notebook.title">Notebook</span> <span class="notebook-count" id="notebookCount">0 / 5</span></span>
<button class="btn-close" id="btnCloseNotebook"></button> <button class="btn-close" id="btnCloseNotebook"></button>
</div> </div>
<div class="notebook-slots" id="notebookSlots"> <div class="notebook-slots" id="notebookSlots">
@@ -105,32 +105,53 @@
<div class="panel-overlay" id="settingsOverlay"></div> <div class="panel-overlay" id="settingsOverlay"></div>
<aside class="settings-panel" id="settingsPanel"> <aside class="settings-panel" id="settingsPanel">
<div class="panel-header"> <div class="panel-header">
<span>Einstellungen</span> <span data-i18n="settings.title">Einstellungen</span>
<button class="btn-close" id="btnCloseSettings"></button> <button class="btn-close" id="btnCloseSettings"></button>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<!-- SPRACHE -->
<section class="settings-section" data-section="language">
<button class="settings-section-title" type="button">
<span class="section-chevron"></span>
<span data-i18n="settings.section.display">DARSTELLUNG</span>
</button>
<div class="section-content">
<div class="setting-row">
<div class="setting-info">
<span class="setting-label" data-i18n="settings.language">Sprache</span>
<span class="setting-desc" data-i18n="settings.language.desc">Anzeigesprache wählen</span>
</div>
<select class="select-input" id="settingLanguage">
<option value="auto" data-i18n="settings.language.auto">Automatisch</option>
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</div>
</div>
</section>
<!-- WIDGETS --> <!-- WIDGETS -->
<section class="settings-section" data-section="widgets"> <section class="settings-section" data-section="widgets">
<button class="settings-section-title" type="button"> <button class="settings-section-title" type="button">
<span class="section-chevron"></span> <span class="section-chevron"></span>
WIDGETS <span data-i18n="settings.section.widgets">WIDGETS</span>
</button> </button>
<div class="section-content"> <div class="section-content">
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Toolbar-Position</span> <span class="setting-label" data-i18n="settings.toolbar_pos">Toolbar-Position</span>
<span class="setting-desc">Widget-Toolbar links oder rechts anzeigen</span> <span class="setting-desc" data-i18n="settings.toolbar_pos.desc">Widget-Toolbar links oder rechts anzeigen</span>
</div> </div>
<select class="select-input" id="settingToolbarPos"> <select class="select-input" id="settingToolbarPos">
<option value="right" selected>Rechts</option> <option value="right" selected data-i18n="settings.toolbar_pos.right">Rechts</option>
<option value="left">Links</option> <option value="left" data-i18n="settings.toolbar_pos.left">Links</option>
</select> </select>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Bild-Referenz Widgets</span> <span class="setting-label" data-i18n="settings.image_ref">Bild-Referenz Widgets</span>
<span class="setting-desc">Bilder als Referenz anzeigen (nur aktuelle Session)</span> <span class="setting-desc" data-i18n="settings.image_ref.desc">Bilder als Referenz anzeigen (nur aktuelle Session)</span>
</div> </div>
<label class="toggle"> <label class="toggle">
<input type="checkbox" id="settingImageRef"> <input type="checkbox" id="settingImageRef">
@@ -144,35 +165,35 @@
<section class="settings-section" data-section="data"> <section class="settings-section" data-section="data">
<button class="settings-section-title" type="button"> <button class="settings-section-title" type="button">
<span class="section-chevron"></span> <span class="section-chevron"></span>
DATEN & HILFE <span data-i18n="settings.section.data">DATEN &amp; HILFE</span>
</button> </button>
<div class="section-content"> <div class="section-content">
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Backup exportieren</span> <span class="setting-label" data-i18n="settings.export">Backup exportieren</span>
<span class="setting-desc">Alle Boards, Notes und Einstellungen als JSON sichern</span> <span class="setting-desc" data-i18n="settings.export.desc">Alle Boards, Notes und Einstellungen als JSON sichern</span>
</div> </div>
<button class="btn-small" id="btnExportJSON">Export</button> <button class="btn-small" id="btnExportJSON" data-i18n="settings.export.btn">Export</button>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Backup importieren</span> <span class="setting-label" data-i18n="settings.import">Backup importieren</span>
<span class="setting-desc">JSON-Backup wiederherstellen</span> <span class="setting-desc" data-i18n="settings.import.desc">JSON-Backup wiederherstellen</span>
</div> </div>
<button class="btn-small" id="btnImportJSON">Import</button> <button class="btn-small" id="btnImportJSON" data-i18n="header.import">Import</button>
<input type="file" id="jsonImportInput" accept=".json" class="hidden" /> <input type="file" id="jsonImportInput" accept=".json" class="hidden" />
</div> </div>
<div class="setting-row" id="browserImportRow"> <div class="setting-row" id="browserImportRow">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Browser-Lesezeichen</span> <span class="setting-label" data-i18n="settings.browser_import">Browser-Lesezeichen</span>
<span class="setting-desc">Lesezeichen direkt aus dem Browser importieren</span> <span class="setting-desc" data-i18n="settings.browser_import.desc">Lesezeichen direkt aus dem Browser importieren</span>
</div> </div>
<button class="btn-small" id="btnBrowserImport">Import</button> <button class="btn-small" id="btnBrowserImport" data-i18n="header.import">Import</button>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Onboarding wiederholen</span> <span class="setting-label" data-i18n="settings.onboarding">Onboarding wiederholen</span>
<span class="setting-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">Start</button>
</div> </div>
@@ -183,13 +204,13 @@
<section class="settings-section" data-section="danger"> <section class="settings-section" data-section="danger">
<button class="settings-section-title danger" type="button"> <button class="settings-section-title danger" type="button">
<span class="section-chevron"></span> <span class="section-chevron"></span>
DANGER ZONE <span data-i18n="settings.section.danger">DANGER ZONE</span>
</button> </button>
<div class="section-content"> <div class="section-content">
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Alles zurücksetzen</span> <span class="setting-label" data-i18n="settings.reset">Alles zurücksetzen</span>
<span class="setting-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">Reset</button>
</div> </div>
@@ -201,13 +222,13 @@
<!-- ABOUT — fixiert am unteren Rand --> <!-- ABOUT — fixiert am unteren Rand -->
<div class="panel-footer"> <div class="panel-footer">
<div class="about-block"> <div class="about-block">
<div class="about-logo">⬡ HELLION NEWTAB</div> <div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div>
<div class="about-version">Version 1.11.1 · by Hellion Online Media</div> <div class="about-version">Version 2.0.0 · 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">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
Impressum <span data-i18n="about.impressum">Impressum</span>
</a> </a>
<a href="https://hellion-media.de" target="_blank" class="about-link"> <a href="https://hellion-media.de" target="_blank" class="about-link">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
@@ -218,26 +239,26 @@
<div class="about-divider"></div> <div class="about-divider"></div>
<div class="about-info-row"> <div class="about-info-row">
<span class="about-info-label">Entwickler</span> <span class="about-info-label" data-i18n="about.developer">Entwickler</span>
<span class="about-info-value">Florian Wathling</span> <span class="about-info-value">Florian Wathling</span>
</div> </div>
<div class="about-info-row"> <div class="about-info-row">
<span class="about-info-label">Unternehmen</span> <span class="about-info-label" data-i18n="about.company">Unternehmen</span>
<span class="about-info-value">Hellion Online Media</span> <span class="about-info-value">Hellion Online Media</span>
</div> </div>
<div class="about-info-row"> <div class="about-info-row">
<span class="about-info-label">Lizenz</span> <span class="about-info-label" data-i18n="about.license">Lizenz</span>
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" class="about-info-value about-link-subtle">CC BY-NC-SA 4.0</a> <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" class="about-info-value about-link-subtle">CC BY-NC-SA 4.0</a>
</div> </div>
<div class="about-info-row"> <div class="about-info-row">
<span class="about-info-label">Datenspeicherung</span> <span class="about-info-label" data-i18n="about.storage">Datenspeicherung</span>
<span class="about-info-value">100% lokal · Kein Server · Kein Account</span> <span class="about-info-value" data-i18n="about.storage.value">100% lokal · Kein Server · Kein Account</span>
</div> </div>
<div class="about-divider"></div> <div class="about-divider"></div>
<div class="about-bugreport"> <div class="about-bugreport">
<span class="about-info-label about-info-label-block">Bug Report / Feedback</span> <span class="about-info-label about-info-label-block" data-i18n="about.bugreport">Bug Report / Feedback</span>
<a href="mailto:kontakt@hellion-media.de?subject=Hellion NewTab Bug Report" class="about-link-mail"> <a href="mailto:kontakt@hellion-media.de?subject=Hellion NewTab Bug Report" class="about-link-mail">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
kontakt@hellion-media.de kontakt@hellion-media.de
@@ -245,7 +266,7 @@
</div> </div>
<div class="about-bugreport"> <div class="about-bugreport">
<span class="about-info-label about-info-label-block">Support</span> <span class="about-info-label about-info-label-block" data-i18n="about.support">Support</span>
<a href="https://ko-fi.com/hellionmedia" target="_blank" class="about-link"> <a href="https://ko-fi.com/hellionmedia" target="_blank" class="about-link">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
Ko-fi — hellionmedia Ko-fi — hellionmedia
@@ -253,7 +274,7 @@
</div> </div>
<div class="about-browsers"> <div class="about-browsers">
<span class="about-info-label about-info-label-block">Kompatible Browser</span> <span class="about-info-label about-info-label-block" data-i18n="about.browsers">Kompatible Browser</span>
<div class="about-browser-tags"> <div class="about-browser-tags">
<span class="browser-tag">Chrome</span> <span class="browser-tag">Chrome</span>
<span class="browser-tag">Edge</span> <span class="browser-tag">Edge</span>
@@ -272,7 +293,7 @@
<div class="modal-overlay" id="themeOverlay"> <div class="modal-overlay" id="themeOverlay">
<div class="theme-modal" id="themeModal"> <div class="theme-modal" id="themeModal">
<div class="modal-header"> <div class="modal-header">
<span>Darstellung</span> <span data-i18n="modal.theme_header">Darstellung</span>
<button class="btn-close" id="btnCloseTheme"></button> <button class="btn-close" id="btnCloseTheme"></button>
</div> </div>
<div class="theme-grid"> <div class="theme-grid">
@@ -333,75 +354,75 @@
</div> </div>
</div> </div>
<div class="theme-modal-section"> <div class="theme-modal-section">
<h3 class="settings-section-title">HINTERGRUND</h3> <h3 class="settings-section-title" data-i18n="settings.section.bg">HINTERGRUND</h3>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Bild-URL</span> <span class="setting-label" data-i18n="settings.bg_url">Bild-URL</span>
<span class="setting-desc">Eigenes Hintergrundbild per URL</span> <span class="setting-desc" data-i18n="settings.bg_url.desc">Eigenes Hintergrundbild per URL</span>
</div> </div>
<button class="btn-small" id="btnChangeBg">Ändern</button> <button class="btn-small" id="btnChangeBg" data-i18n="settings.bg_change">Ändern</button>
</div> </div>
<div class="setting-row hidden" id="bgInputRow"> <div class="setting-row hidden" id="bgInputRow">
<input type="text" class="text-input full-width" id="bgUrlInput" placeholder="https://... oder leer für Standard" /> <input type="text" class="text-input full-width" id="bgUrlInput" placeholder="https://... oder leer für Standard" />
<button class="btn-small" id="btnApplyBg">Übernehmen</button> <button class="btn-small" id="btnApplyBg" data-i18n="settings.bg_apply">Übernehmen</button>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Datei hochladen</span> <span class="setting-label" data-i18n="settings.bg_upload">Datei hochladen</span>
<span class="setting-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">Upload</button>
<input type="file" id="bgFileInput" accept="image/*" class="hidden" /> <input type="file" id="bgFileInput" accept="image/*" class="hidden" />
</div> </div>
</div> </div>
<div class="theme-modal-section"> <div class="theme-modal-section">
<h3 class="settings-section-title">DARSTELLUNG</h3> <h3 class="settings-section-title" data-i18n="settings.section.display">DARSTELLUNG</h3>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Kompaktmodus</span> <span class="setting-label" data-i18n="settings.compact">Kompaktmodus</span>
<span class="setting-desc">Weniger Abstand für mehr Bookmarks</span> <span class="setting-desc" data-i18n="settings.compact.desc">Weniger Abstand für mehr Bookmarks</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingCompact" /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingCompact" /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Lange Titel kürzen</span> <span class="setting-label" data-i18n="settings.shorten">Lange Titel kürzen</span>
<span class="setting-desc">Titel auf eine Zeile mit „…" kürzen</span> <span class="setting-desc" data-i18n="settings.shorten.desc">Titel auf eine Zeile mit „…" kürzen</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingShorten" /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingShorten" /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Suchleiste anzeigen</span> <span class="setting-label" data-i18n="settings.search">Suchleiste anzeigen</span>
<span class="setting-desc">Suchleiste unter dem Header ein/aus</span> <span class="setting-desc" data-i18n="settings.search.desc">Suchleiste unter dem Header ein/aus</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingShowSearch" checked /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingShowSearch" checked /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Links in neuem Tab</span> <span class="setting-label" data-i18n="settings.newtab">Links in neuem Tab</span>
<span class="setting-desc">Bookmarks in neuem Browser-Tab öffnen</span> <span class="setting-desc" data-i18n="settings.newtab.desc">Bookmarks in neuem Browser-Tab öffnen</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingNewTab" checked /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingNewTab" checked /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Beschreibungen anzeigen</span> <span class="setting-label" data-i18n="settings.showdesc">Beschreibungen anzeigen</span>
<span class="setting-desc">Gespeicherte Beschreibung unter Bookmarks</span> <span class="setting-desc" data-i18n="settings.showdesc.desc">Gespeicherte Beschreibung unter Bookmarks</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingShowDesc" /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingShowDesc" /><span class="slider"></span></label>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Bookmarks ausblenden</span> <span class="setting-label" data-i18n="settings.hideextra">Bookmarks ausblenden</span>
<span class="setting-desc">Überzählige Bookmarks in langen Boards verstecken</span> <span class="setting-desc" data-i18n="settings.hideextra.desc">Überzählige Bookmarks in langen Boards verstecken</span>
</div> </div>
<label class="toggle"><input type="checkbox" id="settingHideExtra" /><span class="slider"></span></label> <label class="toggle"><input type="checkbox" id="settingHideExtra" /><span class="slider"></span></label>
</div> </div>
<div class="setting-row" id="visibleCountRow"> <div class="setting-row" id="visibleCountRow">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Sichtbare Bookmarks</span> <span class="setting-label" data-i18n="settings.visible_count">Sichtbare Bookmarks</span>
<span class="setting-desc">Anzahl vor dem Ausblenden</span> <span class="setting-desc" data-i18n="settings.visible_count.desc">Anzahl vor dem Ausblenden</span>
</div> </div>
<select class="select-input" id="settingVisibleCount"> <select class="select-input" id="settingVisibleCount">
<option value="5">5</option> <option value="5">5</option>
@@ -417,14 +438,14 @@
<div class="modal-overlay" id="addBoardOverlay"> <div class="modal-overlay" id="addBoardOverlay">
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<span>New Board</span> <span data-i18n="modal.new_board">New Board</span>
<button class="btn-close" id="btnCancelBoard"></button> <button class="btn-close" id="btnCancelBoard"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="text" class="text-input full-width" id="newBoardName" placeholder="Board name..." maxlength="40" /> <input type="text" class="text-input full-width" id="newBoardName" data-i18n-placeholder="modal.board_name" placeholder="Board name..." maxlength="40" />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-primary" id="btnConfirmBoard">Create</button> <button class="btn-primary" id="btnConfirmBoard" data-i18n="modal.create">Create</button>
</div> </div>
</div> </div>
</div> </div>
@@ -433,16 +454,16 @@
<div class="modal-overlay" id="addBookmarkOverlay"> <div class="modal-overlay" id="addBookmarkOverlay">
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<span>New Bookmark</span> <span data-i18n="modal.new_bookmark">New Bookmark</span>
<button class="btn-close" id="btnCancelBookmark"></button> <button class="btn-close" id="btnCancelBookmark"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="text" class="text-input full-width" id="newBmTitle" placeholder="Title..." maxlength="60" /> <input type="text" class="text-input full-width" id="newBmTitle" data-i18n-placeholder="modal.bm_title" placeholder="Title..." maxlength="60" />
<input type="url" class="text-input full-width modal-input-spaced" id="newBmUrl" placeholder="https://..." /> <input type="url" class="text-input full-width modal-input-spaced" id="newBmUrl" placeholder="https://..." />
<input type="text" class="text-input full-width modal-input-spaced" id="newBmDesc" placeholder="Description (optional)" /> <input type="text" class="text-input full-width modal-input-spaced" id="newBmDesc" data-i18n-placeholder="modal.bm_desc" placeholder="Description (optional)" />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-primary" id="btnConfirmBookmark">Add</button> <button class="btn-primary" id="btnConfirmBookmark" data-i18n="modal.bm_add">Add</button>
</div> </div>
</div> </div>
</div> </div>
@@ -451,14 +472,14 @@
<div class="modal-overlay" id="renameOverlay"> <div class="modal-overlay" id="renameOverlay">
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<span>Rename</span> <span data-i18n="modal.rename">Rename</span>
<button class="btn-close" id="btnCancelRename"></button> <button class="btn-close" id="btnCancelRename"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="text" class="text-input full-width" id="renameInput" placeholder="New name..." maxlength="60" /> <input type="text" class="text-input full-width" id="renameInput" data-i18n-placeholder="modal.rename_placeholder" placeholder="New name..." maxlength="60" />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-primary" id="btnConfirmRename">Rename</button> <button class="btn-primary" id="btnConfirmRename" data-i18n="modal.rename_confirm">Rename</button>
</div> </div>
</div> </div>
</div> </div>
@@ -472,6 +493,8 @@
<script src="src/js/storage.js"></script> <script src="src/js/storage.js"></script>
<!-- State & Hilfsfunktionen --> <!-- State & Hilfsfunktionen -->
<script src="src/js/state.js"></script> <script src="src/js/state.js"></script>
<!-- i18n (nach state.js, vor allen Modulen die t() nutzen könnten) -->
<script src="src/js/i18n.js"></script>
<!-- Dialog-System (vor Features, wird überall gebraucht) --> <!-- Dialog-System (vor Features, wird überall gebraucht) -->
<script src="src/js/dialog.js"></script> <script src="src/js/dialog.js"></script>
<!-- Theme-System --> <!-- Theme-System -->
+11 -10
View File
@@ -10,6 +10,7 @@ async function init() {
boards = savedBoards ?? getDefaultBoards(); boards = savedBoards ?? getDefaultBoards();
if (savedSettings) Object.assign(settings, savedSettings); if (savedSettings) Object.assign(settings, savedSettings);
I18n.init();
applySettings(); applySettings();
renderBoards(); renderBoards();
startClock(); startClock();
@@ -94,8 +95,8 @@ async function checkBackupReminder() {
if (boards.length === 0) return; if (boards.length === 0) return;
const doBackup = await HellionDialog.confirm( const doBackup = await HellionDialog.confirm(
'Du hast seit über einer Woche kein Backup gemacht. Beim Löschen der Browserdaten gehen deine Boards verloren. Jetzt sichern?', t('app.backup_reminder'),
{ type: 'warning', title: 'Backup-Erinnerung', confirmText: 'Jetzt sichern', cancelText: 'Später' } { type: 'warning', title: t('app.backup_reminder.title'), confirmText: t('app.backup_now'), cancelText: t('app.backup_later') }
); );
if (doBackup) { if (doBackup) {
@@ -104,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: '1.11.1', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory, timerPresets }; const data = { version: '2.0.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');
@@ -120,15 +121,15 @@ async function checkBackupReminder() {
// ---- CLOCK & DATE ---- // ---- CLOCK & DATE ----
function startClock() { function startClock() {
const DAYS = ['So','Mo','Di','Mi','Do','Fr','Sa']; const DAY_KEYS = ['clock.days.sun','clock.days.mon','clock.days.tue','clock.days.wed','clock.days.thu','clock.days.fri','clock.days.sat'];
const MONTHS = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez']; const MONTH_KEYS = ['clock.months.jan','clock.months.feb','clock.months.mar','clock.months.apr','clock.months.may','clock.months.jun','clock.months.jul','clock.months.aug','clock.months.sep','clock.months.oct','clock.months.nov','clock.months.dec'];
function tick() { function tick() {
const now = new Date(); const now = new Date();
document.getElementById('clock').textContent = document.getElementById('clock').textContent =
`${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`; `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
document.getElementById('date').textContent = document.getElementById('date').textContent =
`${DAYS[now.getDay()]}, ${String(now.getDate()).padStart(2,'0')}. ${MONTHS[now.getMonth()]}`; `${t(DAY_KEYS[now.getDay()])}, ${String(now.getDate()).padStart(2,'0')}. ${t(MONTH_KEYS[now.getMonth()])}`;
} }
tick(); tick();
setInterval(tick, 1000); setInterval(tick, 1000);
@@ -148,7 +149,7 @@ function bindGlobalEvents() {
if (!file) return; if (!file) return;
const imported = parseBookmarkHtml(await file.text()); const imported = parseBookmarkHtml(await file.text());
if (imported.length === 0) { if (imported.length === 0) {
await HellionDialog.alert('Keine Bookmarks in dieser Datei gefunden.', { type: 'warning', title: 'Import' }); await HellionDialog.alert(t('app.no_bookmarks'), { type: 'warning', title: t('app.import_title') });
return; return;
} }
boards = [...boards, ...imported]; boards = [...boards, ...imported];
@@ -156,8 +157,8 @@ function bindGlobalEvents() {
renderBoards(); renderBoards();
e.target.value = ''; e.target.value = '';
await HellionDialog.alert( await HellionDialog.alert(
`${imported.length} Board(s) mit ${imported.reduce((s,b) => s + b.bookmarks.length, 0)} Bookmarks importiert.`, t('app.html_import_success', { count: imported.length, total: imported.reduce((s,b) => s + b.bookmarks.length, 0) }),
{ type: 'success', title: 'Import erfolgreich' } { type: 'success', title: t('app.import_success_title') }
); );
}); });
@@ -189,7 +190,7 @@ function bindGlobalEvents() {
const url = document.getElementById('newBmUrl').value.trim(); const url = document.getElementById('newBmUrl').value.trim();
const desc = document.getElementById('newBmDesc').value.trim(); const desc = document.getElementById('newBmDesc').value.trim();
if (!title || !url) return; if (!title || !url) return;
try { new URL(url); } catch { await HellionDialog.alert('Ungültige URL. Bitte mit https:// beginnen.', { type: 'warning', title: 'URL ungültig' }); return; } try { new URL(url); } catch { await HellionDialog.alert(t('app.invalid_url'), { type: 'warning', title: t('app.invalid_url.title') }); return; }
const board = boards.find(b => b.id === pendingBookmarkBoardId); const board = boards.find(b => b.id === pendingBookmarkBoardId);
if (!board) return; if (!board) return;
board.bookmarks.push({ id: uid(), title, url, desc }); board.bookmarks.push({ id: uid(), title, url, desc });
+16 -16
View File
@@ -57,14 +57,14 @@ function renderBoards() {
const boardStrong = document.createElement('strong'); const boardStrong = document.createElement('strong');
boardStrong.className = 'accent-text'; boardStrong.className = 'accent-text';
boardStrong.textContent = '+ Board'; boardStrong.textContent = t('boards.add_board');
const importStrong = document.createElement('strong'); const importStrong = document.createElement('strong');
importStrong.className = 'accent-text'; importStrong.className = 'accent-text';
importStrong.textContent = 'Import'; importStrong.textContent = t('boards.import');
empty.append( empty.append(
'No boards yet. Click ', boardStrong, ' to create one, or use ', importStrong, ' to load your browser bookmarks.' t('boards.empty_state_pre'), boardStrong, t('boards.empty_state_mid'), importStrong, t('boards.empty_state_post')
); );
wrapper.appendChild(empty); wrapper.appendChild(empty);
return; return;
@@ -85,7 +85,7 @@ function createBoardEl(board) {
const dragHandle = document.createElement('span'); const dragHandle = document.createElement('span');
dragHandle.className = 'board-drag-handle'; dragHandle.className = 'board-drag-handle';
dragHandle.title = 'Board verschieben'; dragHandle.title = t('boards.drag_title');
dragHandle.appendChild(createDragHandleSvg()); dragHandle.appendChild(createDragHandleSvg());
const titleSpanHeader = document.createElement('span'); const titleSpanHeader = document.createElement('span');
@@ -98,17 +98,17 @@ function createBoardEl(board) {
const btnBlur = document.createElement('button'); const btnBlur = document.createElement('button');
btnBlur.className = 'board-action-btn btn-blur-board'; btnBlur.className = 'board-action-btn btn-blur-board';
btnBlur.title = board.blurred ? 'Unblur' : 'Blur (privat)'; btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
btnBlur.textContent = '\uD83D\uDD12'; btnBlur.textContent = '\uD83D\uDD12';
const btnRename = document.createElement('button'); const btnRename = document.createElement('button');
btnRename.className = 'board-action-btn btn-rename-board'; btnRename.className = 'board-action-btn btn-rename-board';
btnRename.title = 'Umbenennen'; btnRename.title = t('boards.rename');
btnRename.textContent = '\u270E'; btnRename.textContent = '\u270E';
const btnDelete = document.createElement('button'); const btnDelete = document.createElement('button');
btnDelete.className = 'board-action-btn btn-delete-board'; btnDelete.className = 'board-action-btn btn-delete-board';
btnDelete.title = 'Löschen'; btnDelete.title = t('boards.delete');
btnDelete.textContent = '\u2715'; btnDelete.textContent = '\u2715';
actions.append(btnBlur, btnRename, btnDelete); actions.append(btnBlur, btnRename, btnDelete);
@@ -123,14 +123,14 @@ function createBoardEl(board) {
e.stopPropagation(); e.stopPropagation();
board.blurred = !board.blurred; board.blurred = !board.blurred;
div.classList.toggle('blurred', board.blurred); div.classList.toggle('blurred', board.blurred);
btnBlur.title = board.blurred ? 'Unblur' : 'Blur (privat)'; btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
await saveBoards(); await saveBoards();
}); });
blurOverlay.addEventListener('click', async () => { blurOverlay.addEventListener('click', async () => {
board.blurred = false; board.blurred = false;
div.classList.remove('blurred'); div.classList.remove('blurred');
btnBlur.title = 'Blur (privat)'; btnBlur.title = t('boards.blur');
await saveBoards(); await saveBoards();
}); });
@@ -147,8 +147,8 @@ function createBoardEl(board) {
btnDelete.addEventListener('click', async e => { btnDelete.addEventListener('click', async e => {
e.stopPropagation(); e.stopPropagation();
const ok = await HellionDialog.confirm( const ok = await HellionDialog.confirm(
`Board "${board.title}" wirklich löschen?`, t('boards.delete_confirm', { title: board.title }),
{ type: 'danger', title: 'Board löschen', confirmText: 'Löschen' } { type: 'danger', title: t('boards.delete_confirm.title'), confirmText: t('boards.delete') }
); );
if (ok) { if (ok) {
boards = boards.filter(b => b.id !== board.id); boards = boards.filter(b => b.id !== board.id);
@@ -180,16 +180,16 @@ function createBoardEl(board) {
let hiddenEls = []; let hiddenEls = [];
const showMoreBtn = document.createElement('button'); const showMoreBtn = document.createElement('button');
showMoreBtn.className = 'show-more-btn'; showMoreBtn.className = 'show-more-btn';
showMoreBtn.textContent = `Show ${hidden.length} more…`; showMoreBtn.textContent = t('boards.show_more', { count: hidden.length });
showMoreBtn.addEventListener('click', () => { showMoreBtn.addEventListener('click', () => {
if (!expanded) { if (!expanded) {
hidden.forEach(bm => { const el = createBmEl(bm); hiddenEls.push(el); list.appendChild(el); }); hidden.forEach(bm => { const el = createBmEl(bm); hiddenEls.push(el); list.appendChild(el); });
showMoreBtn.textContent = 'Show less'; showMoreBtn.textContent = t('boards.show_less');
expanded = true; expanded = true;
} else { } else {
hiddenEls.forEach(el => el.remove()); hiddenEls.forEach(el => el.remove());
hiddenEls = []; hiddenEls = [];
showMoreBtn.textContent = `Show ${hidden.length} more…`; showMoreBtn.textContent = t('boards.show_more', { count: hidden.length });
expanded = false; expanded = false;
} }
}); });
@@ -200,7 +200,7 @@ function createBoardEl(board) {
const addBtn = document.createElement('button'); const addBtn = document.createElement('button');
addBtn.className = 'add-bm-btn'; addBtn.className = 'add-bm-btn';
addBtn.appendChild(createPlusSvg()); addBtn.appendChild(createPlusSvg());
addBtn.append(' Add link'); addBtn.append(t('boards.add_link'));
addBtn.addEventListener('click', () => openAddBookmarkModal(board.id)); addBtn.addEventListener('click', () => openAddBookmarkModal(board.id));
div.appendChild(addBtn); div.appendChild(addBtn);
@@ -243,7 +243,7 @@ function createBmEl(bm) {
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = 'bm-delete'; deleteBtn.className = 'bm-delete';
deleteBtn.title = 'Entfernen'; deleteBtn.title = t('boards.remove_bookmark');
deleteBtn.textContent = '✕'; deleteBtn.textContent = '✕';
li.appendChild(favicon); li.appendChild(favicon);
+19 -19
View File
@@ -42,8 +42,8 @@ const BrowserBookmarkImport = {
tree = await api.getTree(); tree = await api.getTree();
} catch (err) { } catch (err) {
await HellionDialog.alert( await HellionDialog.alert(
'Zugriff auf Browser-Lesezeichen nicht möglich. Stelle sicher, dass die Extension die nötigen Berechtigungen hat.', t('bm_import.no_access'),
{ type: 'warning', title: 'Lesezeichen-Import' } { type: 'warning', title: t('bm_import.title') }
); );
return; return;
} }
@@ -51,8 +51,8 @@ const BrowserBookmarkImport = {
const folders = this._extractFolders(tree[0]); const folders = this._extractFolders(tree[0]);
if (folders.length === 0) { if (folders.length === 0) {
await HellionDialog.alert( await HellionDialog.alert(
'Keine Lesezeichen-Ordner gefunden.', t('bm_import.no_folders'),
{ type: 'warning', title: 'Lesezeichen-Import' } { type: 'warning', title: t('bm_import.title') }
); );
return; return;
} }
@@ -79,7 +79,7 @@ const BrowserBookmarkImport = {
result.push({ result.push({
id: child.id, id: child.id,
title: child.title || 'Unbenannt', title: child.title || t('bm_import.unnamed'),
depth: depth, depth: depth,
bookmarkCount: bookmarkCount, bookmarkCount: bookmarkCount,
subfolderCount: subfolderCount, subfolderCount: subfolderCount,
@@ -114,7 +114,7 @@ const BrowserBookmarkImport = {
header.className = 'bm-import-header'; header.className = 'bm-import-header';
const title = document.createElement('span'); const title = document.createElement('span');
title.textContent = 'Browser-Lesezeichen importieren'; title.textContent = t('bm_import.modal_title');
header.appendChild(title); header.appendChild(title);
const closeBtn = document.createElement('button'); const closeBtn = document.createElement('button');
@@ -128,7 +128,7 @@ const BrowserBookmarkImport = {
// Info // Info
const info = document.createElement('div'); const info = document.createElement('div');
info.className = 'bm-import-info'; info.className = 'bm-import-info';
info.textContent = 'Wähle die Ordner aus, die als Boards importiert werden sollen. Jeder Ordner wird ein eigenes Board.'; info.textContent = t('bm_import.info');
modal.appendChild(info); modal.appendChild(info);
// Ordner-Liste // Ordner-Liste
@@ -155,13 +155,13 @@ const BrowserBookmarkImport = {
meta.className = 'bm-import-folder-meta'; meta.className = 'bm-import-folder-meta';
const parts = []; const parts = [];
if (folder.bookmarkCount > 0) { if (folder.bookmarkCount > 0) {
parts.push(folder.bookmarkCount + ' Link' + (folder.bookmarkCount !== 1 ? 's' : '')); parts.push(t('bm_import.link_count', { count: folder.bookmarkCount }));
} }
if (folder.subfolderCount > 0) { if (folder.subfolderCount > 0) {
parts.push(folder.subfolderCount + ' Ordner'); parts.push(t('bm_import.folder_count', { count: folder.subfolderCount }));
} }
if (parts.length === 0) { if (parts.length === 0) {
parts.push('leer'); parts.push(t('bm_import.empty'));
} }
meta.textContent = parts.join(', '); meta.textContent = parts.join(', ');
row.appendChild(meta); row.appendChild(meta);
@@ -177,18 +177,18 @@ const BrowserBookmarkImport = {
const selectAll = document.createElement('button'); const selectAll = document.createElement('button');
selectAll.className = 'btn-secondary'; selectAll.className = 'btn-secondary';
selectAll.textContent = 'Alle auswählen'; selectAll.textContent = t('bm_import.select_all');
selectAll.addEventListener('click', () => { selectAll.addEventListener('click', () => {
const boxes = list.querySelectorAll('.bm-import-checkbox'); const boxes = list.querySelectorAll('.bm-import-checkbox');
const allChecked = Array.from(boxes).every(function(cb) { return cb.checked; }); const allChecked = Array.from(boxes).every(function(cb) { return cb.checked; });
boxes.forEach(function(cb) { cb.checked = !allChecked; }); boxes.forEach(function(cb) { cb.checked = !allChecked; });
selectAll.textContent = allChecked ? 'Alle auswählen' : 'Alle abwählen'; selectAll.textContent = allChecked ? t('bm_import.select_all') : t('bm_import.deselect_all');
}); });
footer.appendChild(selectAll); footer.appendChild(selectAll);
const importBtn = document.createElement('button'); const importBtn = document.createElement('button');
importBtn.className = 'btn-primary'; importBtn.className = 'btn-primary';
importBtn.textContent = 'Importieren'; importBtn.textContent = t('bm_import.import_btn');
importBtn.addEventListener('click', () => this._importSelected(folders)); importBtn.addEventListener('click', () => this._importSelected(folders));
footer.appendChild(importBtn); footer.appendChild(importBtn);
@@ -216,8 +216,8 @@ const BrowserBookmarkImport = {
const checkboxes = document.querySelectorAll('.bm-import-checkbox:checked'); const checkboxes = document.querySelectorAll('.bm-import-checkbox:checked');
if (checkboxes.length === 0) { if (checkboxes.length === 0) {
await HellionDialog.alert( await HellionDialog.alert(
'Bitte wähle mindestens einen Ordner aus.', t('bm_import.no_selection'),
{ type: 'warning', title: 'Lesezeichen-Import' } { type: 'warning', title: t('bm_import.title') }
); );
return; return;
} }
@@ -289,15 +289,15 @@ const BrowserBookmarkImport = {
// Ergebnis-Dialog // Ergebnis-Dialog
const lines = []; const lines = [];
lines.push(boardsCreated + ' Board' + (boardsCreated !== 1 ? 's' : '') + ' erstellt'); lines.push(t('bm_import.boards_created', { count: boardsCreated }));
lines.push(totalImported + ' Lesezeichen importiert'); lines.push(t('bm_import.bookmarks_imported', { count: totalImported }));
if (totalSkipped > 0) { if (totalSkipped > 0) {
lines.push(totalSkipped + ' Duplikat' + (totalSkipped !== 1 ? 'e' : '') + ' übersprungen'); lines.push(t('bm_import.duplicates_skipped', { count: totalSkipped }));
} }
await HellionDialog.alert( await HellionDialog.alert(
lines.join('\n'), lines.join('\n'),
{ type: 'success', title: 'Import abgeschlossen' } { type: 'success', title: t('bm_import.success_title') }
); );
} }
}; };
+3 -3
View File
@@ -69,7 +69,7 @@ const Calculator = {
const widgetId = WidgetManager.create('calculator', { const widgetId = WidgetManager.create('calculator', {
id: this.WIDGET_ID, id: this.WIDGET_ID,
title: 'Taschenrechner', title: t('calculator.title'),
x: saved.x || 400, x: saved.x || 400,
y: saved.y || 120, y: saved.y || 120,
width: saved.width || 280, width: saved.width || 280,
@@ -214,7 +214,7 @@ const Calculator = {
const title = document.createElement('div'); const title = document.createElement('div');
title.className = 'calc-history-title'; title.className = 'calc-history-title';
title.textContent = 'History'; title.textContent = t('calculator.history');
container.appendChild(title); container.appendChild(title);
this._renderHistoryItems(container); this._renderHistoryItems(container);
@@ -345,7 +345,7 @@ const Calculator = {
const result = this._evaluate(this._currentExpr); const result = this._evaluate(this._currentExpr);
if (result === null) { if (result === null) {
this._lastResult = 'Fehler'; this._lastResult = t('calculator.error');
this._updateDisplay(); this._updateDisplay();
return; return;
} }
+11 -11
View File
@@ -13,7 +13,7 @@ function initDataButtons() {
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: '1.11.1', version: '2.0.0',
exported: new Date().toISOString(), exported: new Date().toISOString(),
boards, boards,
settings, settings,
@@ -37,7 +37,7 @@ function initDataButtons() {
if (!file) return; if (!file) return;
try { try {
const data = JSON.parse(await file.text()); const data = JSON.parse(await file.text());
if (!Array.isArray(data.boards)) throw new Error('Ungültiges Format'); if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
const validBoards = data.boards.filter(b => { const validBoards = data.boards.filter(b => {
if (!b || typeof b.title !== 'string' || !Array.isArray(b.bookmarks)) return false; if (!b || typeof b.title !== 'string' || !Array.isArray(b.bookmarks)) return false;
b.id = b.id || uid(); b.id = b.id || uid();
@@ -50,10 +50,10 @@ function initDataButtons() {
}); });
return true; return true;
}); });
if (validBoards.length === 0) throw new Error('Keine gültigen Boards gefunden'); if (validBoards.length === 0) throw new Error(t('data.no_boards'));
const ok = await HellionDialog.confirm( const ok = await HellionDialog.confirm(
`${validBoards.length} Boards importieren? Bestehende Daten bleiben erhalten.`, t('data.import_confirm', { count: validBoards.length }),
{ type: 'info', title: 'JSON Import' } { type: 'info', title: t('data.import_confirm.title') }
); );
if (!ok) return; if (!ok) return;
boards = [...boards, ...validBoards]; boards = [...boards, ...validBoards];
@@ -112,15 +112,15 @@ function initDataButtons() {
// 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 ? t('data.notes_suffix', { count: notesImported }) : '';
const calcMsg = calcImported ? ' + Calculator-History' : ''; const calcMsg = calcImported ? t('data.calc_suffix') : '';
const timerMsg = timerImported ? ' + Timer-Presets' : ''; const timerMsg = timerImported ? t('data.timer_suffix') : '';
await HellionDialog.alert( await HellionDialog.alert(
`${validBoards.length} Board(s)${noteMsg}${calcMsg}${timerMsg} erfolgreich importiert.`, t('data.import_success', { boards: validBoards.length, notes: noteMsg, calc: calcMsg, timer: timerMsg }),
{ type: 'success', title: 'Import erfolgreich' } { type: 'success', title: t('data.import_success.title') }
); );
} catch (err) { } catch (err) {
await HellionDialog.alert('Fehler beim Import: ' + err.message, { type: 'danger', title: 'Import fehlgeschlagen' }); await HellionDialog.alert(t('data.import_error', { error: err.message }), { type: 'danger', title: t('data.import_error.title') });
} }
e.target.value = ''; e.target.value = '';
}); });
+5 -5
View File
@@ -126,8 +126,8 @@ const HellionDialog = {
const opts = options || {}; const opts = options || {};
return this._show({ return this._show({
message, message,
title: opts.title || 'Hinweis', title: opts.title || t('dialog.default_title'),
confirmText: opts.confirmText || 'OK', confirmText: opts.confirmText || t('dialog.ok'),
cancelText: '', cancelText: '',
type: opts.type || 'info', type: opts.type || 'info',
isConfirm: false isConfirm: false
@@ -144,9 +144,9 @@ const HellionDialog = {
const opts = options || {}; const opts = options || {};
return this._show({ return this._show({
message, message,
title: opts.title || 'Bestätigung', title: opts.title || t('dialog.confirm_title'),
confirmText: opts.confirmText || 'OK', confirmText: opts.confirmText || t('dialog.ok'),
cancelText: opts.cancelText || 'Abbrechen', cancelText: opts.cancelText || t('dialog.cancel'),
type: opts.type || 'info', type: opts.type || 'info',
isConfirm: true isConfirm: true
}); });
+666
View File
@@ -0,0 +1,666 @@
/* =============================================
HELLION NEWTAB — i18n.js
Internationalisierung: DE/EN Sprachumschaltung
============================================= */
const STRINGS = {
de: {
// Dialog-System
'dialog.default_title': 'Hinweis',
'dialog.ok': 'OK',
'dialog.confirm_title': 'Bestätigung',
'dialog.cancel': 'Abbrechen',
// Boards
'boards.empty_state_pre': 'Noch keine Boards. Klicke auf ',
'boards.add_board': '+ Board',
'boards.empty_state_mid': ' um eins zu erstellen, oder nutze ',
'boards.import': 'Import',
'boards.empty_state_post': ' um deine Browser-Lesezeichen zu laden.',
'boards.drag_title': 'Board verschieben',
'boards.blur': 'Blur (privat)',
'boards.unblur': 'Unblur',
'boards.rename': 'Umbenennen',
'boards.delete': 'Löschen',
'boards.delete_confirm': 'Board „{title}" wirklich löschen?',
'boards.delete_confirm.title': 'Board löschen',
'boards.show_more': '{count} weitere anzeigen…',
'boards.show_less': 'Weniger anzeigen',
'boards.add_link': ' Link hinzufügen',
'boards.remove_bookmark': 'Entfernen',
// Onboarding
'onboarding.skip': 'Überspringen',
'onboarding.back': 'Zurück',
'onboarding.next': 'Weiter',
'onboarding.start': 'Los geht\'s!',
'onboarding.yes': 'Ja, gerne',
'onboarding.no': 'Nein danke',
'onboarding.s1.title': 'Willkommen bei Hellion Dashboard',
'onboarding.s1.text': 'Dein neuer Browser-Startbildschirm. Minimalistisch, schnell und vollständig lokal — keine Cloud, kein Account, keine Datensammlung.',
'onboarding.s2.title': 'Boards & Bookmarks',
'onboarding.s2.f1': 'Erstelle Boards mit dem „+ Board" Button oben',
'onboarding.s2.f2': 'Importiere Browser-Lesezeichen über den „Import" Button im Header',
'onboarding.s2.f3': 'Drag & Drop zum Umsortieren von Boards und Links',
'onboarding.s2.f4': 'Blur-Modus für private Boards (🔒 Icon)',
'onboarding.s3.title': '11 handgefertigte Themes',
'onboarding.s3.text': 'Klicke auf den „Theme" Button im Header um dein Theme zu wählen. Jedes hat seinen eigenen Stil und Farbpalette.',
'onboarding.s4.title': 'Widget-Toolbar',
'onboarding.s4.f1': 'Die schwebenden Buttons rechts öffnen Widgets',
'onboarding.s4.f2': 'Notes und Checklisten für schnelle Notizen',
'onboarding.s4.f3': 'Taschenrechner mit History',
'onboarding.s4.f4': 'Timer/Countdown mit speicherbaren Presets',
'onboarding.s4.f5': 'Bild-Referenz Widgets (aktivierbar in Settings)',
'onboarding.s4.f6': 'Notebook-Sidebar zeigt alle Notes auf einen Blick',
'onboarding.s5.title': 'Backups nicht vergessen!',
'onboarding.s5.text': 'Deine Daten sind lokal im Browser gespeichert. Wenn du Browserdaten löschst, gehen sie verloren! Sichere regelmäßig über Settings → Data → Export. Wir erinnern dich alle 7 Tage daran.',
'onboarding.s6.title': 'Gaming Starter Board',
'onboarding.s6.text': 'Spielst du Games wie Satisfactory, Factorio oder Star Citizen? Ich kann ein Board mit nützlichen Community-Links anlegen.',
'onboarding.s7.title': 'Bereit!',
'onboarding.s7.text': 'Erstelle dein erstes Board mit „+ Board" oder importiere deine Browser-Lesezeichen über den Import-Button im Header. Viel Spaß!',
// Notes
'notes.limit_message': 'Maximale Anzahl erreicht! Du kannst maximal {max} Notes gleichzeitig haben. Lösche eine bestehende Note um eine neue zu erstellen.',
'notes.limit_title': 'Limit erreicht',
'notes.checklist_title': 'Checkliste',
'notes.default_title': 'Note',
'notes.placeholder': 'Notiz schreiben...',
'notes.checklist_placeholder': 'Neues Item...',
'notes.delete_confirm': 'Note endgültig löschen? Das kann nicht rückgängig gemacht werden.',
'notes.delete_title': 'Note löschen',
'notes.delete_button': 'Löschen',
'notes.checklist_progress': '{done}/{total} erledigt',
'notes.empty_preview': 'Leer',
'notes.export': 'Export',
'notes.export_footer': 'Exportiert aus Hellion Dashboard',
'notes.create': '+ Note erstellen',
'notes.text_type': '✎ Freitext',
'notes.checklist_type': '☑ Checkliste',
// Calculator
'calculator.title': 'Taschenrechner',
'calculator.history': 'History',
'calculator.error': 'Fehler',
// Timer
'timer.title': 'Timer',
'timer.start': 'Start',
'timer.pause': 'Pause',
'timer.reset': 'Reset',
'timer.restart': 'Neustart',
'timer.presets': 'Presets',
'timer.save_preset': 'Preset speichern',
'timer.preset_name_placeholder': 'Name...',
'timer.ok': 'OK',
'timer.limit_title': 'Limit erreicht',
'timer.limit_message': 'Maximale Anzahl erreicht! Du kannst maximal {max} Presets speichern.',
'timer.no_time_title': 'Keine Zeit',
'timer.no_time_message': 'Gib zuerst eine Zeit ein, bevor du ein Preset speicherst.',
'timer.mute': 'Ton ausschalten',
'timer.unmute': 'Ton einschalten',
'timer.finished_title': '[!] Timer abgelaufen',
'timer.default_page_title': 'Hellion Dashboard',
// Bild-Referenz
'imageref.title': 'Bild-Referenz',
'imageref.dropzone': 'Klicken oder Bild hierher ziehen',
'imageref.replace': 'Bild ersetzen',
'imageref.label_placeholder': 'Beschriftung (optional)',
'imageref.storage_error': 'Bild konnte nicht gespeichert werden. Der Browser-Speicher ist voll.',
'imageref.storage_error.title': 'Speicherfehler',
'imageref.limit': 'Maximal {max} Bild-Widgets gleichzeitig. Schliesse eines um ein neues zu oeffnen.',
'imageref.limit.title': 'Limit erreicht',
'imageref.load_error': 'Bild konnte nicht geladen werden: {error}',
'imageref.load_error.title': 'Bildfehler',
'imageref.invalid_file': 'Bitte eine Bilddatei verwenden (PNG, JPG, WebP, etc.).',
'imageref.invalid_file.title': 'Kein Bild',
// Widget-Manager
'widget.minimize': 'Minimieren',
'widget.close': 'Schließen',
// Daten (Export/Import)
'data.invalid_format': 'Ungültiges Format',
'data.no_boards': 'Keine gültigen Boards gefunden',
'data.import_confirm': '{count} Boards importieren? Bestehende Daten bleiben erhalten.',
'data.import_confirm.title': 'JSON Import',
'data.import_success': '{boards} Board(s){notes}{calc}{timer} erfolgreich importiert.',
'data.import_success.title': 'Import erfolgreich',
'data.import_error': 'Fehler beim Import: {error}',
'data.import_error.title': 'Import fehlgeschlagen',
'data.notes_suffix': ' + {count} Note(s)',
'data.calc_suffix': ' + Calculator-History',
'data.timer_suffix': ' + Timer-Presets',
// Browser-Lesezeichen Import
'bm_import.no_access': 'Zugriff auf Browser-Lesezeichen nicht möglich. Stelle sicher, dass die Extension die nötigen Berechtigungen hat.',
'bm_import.title': 'Lesezeichen-Import',
'bm_import.no_folders': 'Keine Lesezeichen-Ordner gefunden.',
'bm_import.modal_title': 'Browser-Lesezeichen importieren',
'bm_import.info': 'Wähle die Ordner aus, die als Boards importiert werden sollen. Jeder Ordner wird ein eigenes Board.',
'bm_import.unnamed': 'Unbenannt',
'bm_import.link_count': '{count} Link(s)',
'bm_import.folder_count': '{count} Ordner',
'bm_import.empty': 'leer',
'bm_import.select_all': 'Alle auswählen',
'bm_import.deselect_all': 'Alle abwählen',
'bm_import.import_btn': 'Importieren',
'bm_import.no_selection': 'Bitte wähle mindestens einen Ordner aus.',
'bm_import.boards_created': '{count} Board(s) erstellt',
'bm_import.bookmarks_imported': '{count} Lesezeichen importiert',
'bm_import.duplicates_skipped': '{count} Duplikat(e) übersprungen',
'bm_import.success_title': 'Import abgeschlossen',
// Storage
'storage.quota_full': 'Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.',
'storage.quota_full.title': 'Speicher voll',
// App
'app.backup_reminder': 'Du hast seit über einer Woche kein Backup gemacht. Beim Löschen der Browserdaten gehen deine Boards verloren. Jetzt sichern?',
'app.backup_reminder.title': 'Backup-Erinnerung',
'app.backup_now': 'Jetzt sichern',
'app.backup_later': 'Später',
'app.no_bookmarks': 'Keine Bookmarks in dieser Datei gefunden.',
'app.import_title': 'Import',
'app.html_import_success': '{count} Board(s) mit {total} Bookmarks importiert.',
'app.import_success_title': 'Import erfolgreich',
'app.invalid_url': 'Ungültige URL. Bitte mit https:// beginnen.',
'app.invalid_url.title': 'URL ungültig',
// Uhr
'clock.days.sun': 'So',
'clock.days.mon': 'Mo',
'clock.days.tue': 'Di',
'clock.days.wed': 'Mi',
'clock.days.thu': 'Do',
'clock.days.fri': 'Fr',
'clock.days.sat': 'Sa',
'clock.months.jan': 'Jan',
'clock.months.feb': 'Feb',
'clock.months.mar': 'Mär',
'clock.months.apr': 'Apr',
'clock.months.may': 'Mai',
'clock.months.jun': 'Jun',
'clock.months.jul': 'Jul',
'clock.months.aug': 'Aug',
'clock.months.sep': 'Sep',
'clock.months.oct': 'Okt',
'clock.months.nov': 'Nov',
'clock.months.dec': 'Dez',
// Settings
'settings.file_read_error': 'Fehler beim Lesen der Datei. Bitte eine andere Datei wählen.',
'settings.file_read_error.title': 'Dateifehler',
'settings.reset_confirm': 'Wirklich alle Boards und Einstellungen löschen? Das kann nicht rückgängig gemacht werden.',
'settings.reset_confirm.title': 'Alles zurücksetzen',
'settings.reset_confirm.button': 'Alles löschen',
// Header
'header.import': 'Import',
'header.board': 'Board',
'header.note': 'Note',
'header.theme': 'Darstellung',
'header.settings': 'Einstellungen',
// Settings-Panel Überschrift
'settings.title': 'Einstellungen',
// Settings-Panel Sektionen
'settings.section.widgets': 'WIDGETS',
'settings.section.data': 'DATEN & HILFE',
'settings.section.danger': 'DANGER ZONE',
'settings.section.bg': 'HINTERGRUND',
'settings.section.display': 'DARSTELLUNG',
// Settings-Zeilen
'settings.language': 'Sprache',
'settings.language.desc': 'Anzeigesprache wählen',
'settings.language.auto': 'Automatisch',
'settings.toolbar_pos': 'Toolbar-Position',
'settings.toolbar_pos.desc': 'Widget-Toolbar links oder rechts anzeigen',
'settings.toolbar_pos.right': 'Rechts',
'settings.toolbar_pos.left': 'Links',
'settings.image_ref': 'Bild-Referenz Widgets',
'settings.image_ref.desc': 'Bilder als Referenz anzeigen (nur aktuelle Session)',
'settings.export': 'Backup exportieren',
'settings.export.desc': 'Alle Boards, Notes und Einstellungen als JSON sichern',
'settings.export.btn': 'Export',
'settings.import': 'Backup importieren',
'settings.import.desc': 'JSON-Backup wiederherstellen',
'settings.browser_import': 'Browser-Lesezeichen',
'settings.browser_import.desc': 'Lesezeichen direkt aus dem Browser importieren',
'settings.onboarding': 'Onboarding wiederholen',
'settings.onboarding.desc': 'Willkommens-Tour erneut anzeigen',
'settings.reset': 'Alles zurücksetzen',
'settings.reset.desc': 'Löscht alle Boards, Notes und Einstellungen',
'settings.compact': 'Kompaktmodus',
'settings.compact.desc': 'Weniger Abstand für mehr Bookmarks',
'settings.shorten': 'Lange Titel kürzen',
'settings.shorten.desc': 'Titel auf eine Zeile mit „…" kürzen',
'settings.search': 'Suchleiste anzeigen',
'settings.search.desc': 'Suchleiste unter dem Header ein/aus',
'settings.newtab': 'Links in neuem Tab',
'settings.newtab.desc': 'Bookmarks in neuem Browser-Tab öffnen',
'settings.showdesc': 'Beschreibungen anzeigen',
'settings.showdesc.desc': 'Gespeicherte Beschreibung unter Bookmarks',
'settings.hideextra': 'Bookmarks ausblenden',
'settings.hideextra.desc': 'Überzählige Bookmarks in langen Boards verstecken',
'settings.visible_count': 'Sichtbare Bookmarks',
'settings.visible_count.desc': 'Anzahl vor dem Ausblenden',
'settings.bg_url': 'Bild-URL',
'settings.bg_url.desc': 'Eigenes Hintergrundbild per URL',
'settings.bg_change': 'Ändern',
'settings.bg_apply': 'Übernehmen',
'settings.bg_upload': 'Datei hochladen',
'settings.bg_upload.desc': 'Lokales Bild als Hintergrund verwenden',
'settings.search_engine_toggle': 'Suchmaschine wechseln',
// Modals
'modal.new_board': 'Neues Board',
'modal.board_name': 'Board-Name...',
'modal.create': 'Erstellen',
'modal.new_bookmark': 'Neues Lesezeichen',
'modal.bm_title': 'Titel...',
'modal.bm_desc': 'Beschreibung (optional)',
'modal.bm_add': 'Hinzufügen',
'modal.rename': 'Umbenennen',
'modal.rename_placeholder': 'Neuer Name...',
'modal.rename_confirm': 'Umbenennen',
'modal.theme_header': 'Darstellung',
// About
'about.title': '⬡ HELLION NEWTAB',
'about.impressum': 'Impressum',
'about.developer': 'Entwickler',
'about.company': 'Unternehmen',
'about.license': 'Lizenz',
'about.storage': 'Datenspeicherung',
'about.storage.value': '100% lokal · Kein Server · Kein Account',
'about.bugreport': 'Bug Report / Feedback',
'about.support': 'Support',
'about.browsers': 'Kompatible Browser',
// Notebook
'notebook.title': 'Notebook',
// Suche
'search.placeholder': 'Im Web suchen…',
// Widget-Toolbar Tooltips
'toolbar.note': 'Note erstellen',
'toolbar.checklist': 'Checkliste erstellen',
'toolbar.calculator': 'Taschenrechner',
'toolbar.timer': 'Timer',
'toolbar.imageref': 'Bild-Referenz',
'toolbar.notebook': 'Alle Notes'
},
en: {
// Dialog system
'dialog.default_title': 'Notice',
'dialog.ok': 'OK',
'dialog.confirm_title': 'Confirmation',
'dialog.cancel': 'Cancel',
// Boards
'boards.empty_state_pre': 'No boards yet. Click ',
'boards.add_board': '+ Board',
'boards.empty_state_mid': ' to create one, or use ',
'boards.import': 'Import',
'boards.empty_state_post': ' to load your browser bookmarks.',
'boards.drag_title': 'Move board',
'boards.blur': 'Blur (private)',
'boards.unblur': 'Unblur',
'boards.rename': 'Rename',
'boards.delete': 'Delete',
'boards.delete_confirm': 'Really delete board "{title}"?',
'boards.delete_confirm.title': 'Delete board',
'boards.show_more': 'Show {count} more…',
'boards.show_less': 'Show less',
'boards.add_link': ' Add link',
'boards.remove_bookmark': 'Remove',
// Onboarding
'onboarding.skip': 'Skip',
'onboarding.back': 'Back',
'onboarding.next': 'Next',
'onboarding.start': 'Let\'s go!',
'onboarding.yes': 'Yes please',
'onboarding.no': 'No thanks',
'onboarding.s1.title': 'Welcome to Hellion Dashboard',
'onboarding.s1.text': 'Your new browser start screen. Minimalist, fast and fully local — no cloud, no account, no data collection.',
'onboarding.s2.title': 'Boards & Bookmarks',
'onboarding.s2.f1': 'Create boards with the "+ Board" button at the top',
'onboarding.s2.f2': 'Import browser bookmarks via the "Import" button in the header',
'onboarding.s2.f3': 'Drag & drop to reorder boards and links',
'onboarding.s2.f4': 'Blur mode for private boards (🔒 icon)',
'onboarding.s3.title': '11 handcrafted themes',
'onboarding.s3.text': 'Click the "Theme" button in the header to choose your theme. Each has its own style and color palette.',
'onboarding.s4.title': 'Widget Toolbar',
'onboarding.s4.f1': 'The floating buttons on the right open widgets',
'onboarding.s4.f2': 'Notes and checklists for quick notes',
'onboarding.s4.f3': 'Calculator with history',
'onboarding.s4.f4': 'Timer/countdown with saveable presets',
'onboarding.s4.f5': 'Image reference widgets (enable in Settings)',
'onboarding.s4.f6': 'Notebook sidebar shows all notes at a glance',
'onboarding.s5.title': 'Don\'t forget backups!',
'onboarding.s5.text': 'Your data is stored locally in the browser. If you clear browser data, it\'s gone! Back up regularly via Settings → Data → Export. We\'ll remind you every 7 days.',
'onboarding.s6.title': 'Gaming Starter Board',
'onboarding.s6.text': 'Do you play games like Satisfactory, Factorio or Star Citizen? I can create a board with useful community links.',
'onboarding.s7.title': 'Ready!',
'onboarding.s7.text': 'Create your first board with "+ Board" or import your browser bookmarks via the Import button in the header. Have fun!',
// Notes
'notes.limit_message': 'Maximum reached! You can have at most {max} notes at the same time. Delete an existing note to create a new one.',
'notes.limit_title': 'Limit reached',
'notes.checklist_title': 'Checklist',
'notes.default_title': 'Note',
'notes.placeholder': 'Write a note...',
'notes.checklist_placeholder': 'New item...',
'notes.delete_confirm': 'Permanently delete note? This cannot be undone.',
'notes.delete_title': 'Delete note',
'notes.delete_button': 'Delete',
'notes.checklist_progress': '{done}/{total} done',
'notes.empty_preview': 'Empty',
'notes.export': 'Export',
'notes.export_footer': 'Exported from Hellion Dashboard',
'notes.create': '+ Create note',
'notes.text_type': '✎ Free text',
'notes.checklist_type': '☑ Checklist',
// Calculator
'calculator.title': 'Calculator',
'calculator.history': 'History',
'calculator.error': 'Error',
// Timer
'timer.title': 'Timer',
'timer.start': 'Start',
'timer.pause': 'Pause',
'timer.reset': 'Reset',
'timer.restart': 'Restart',
'timer.presets': 'Presets',
'timer.save_preset': 'Save preset',
'timer.preset_name_placeholder': 'Name...',
'timer.ok': 'OK',
'timer.limit_title': 'Limit reached',
'timer.limit_message': 'Maximum reached! You can save at most {max} presets.',
'timer.no_time_title': 'No time',
'timer.no_time_message': 'Enter a time before saving a preset.',
'timer.mute': 'Mute sound',
'timer.unmute': 'Unmute sound',
'timer.finished_title': '[!] Timer finished',
'timer.default_page_title': 'Hellion Dashboard',
// Image reference
'imageref.title': 'Image Reference',
'imageref.dropzone': 'Click or drag image here',
'imageref.replace': 'Replace image',
'imageref.label_placeholder': 'Caption (optional)',
'imageref.storage_error': 'Image could not be saved. Browser storage is full.',
'imageref.storage_error.title': 'Storage error',
'imageref.limit': 'Maximum {max} image widgets at a time. Close one to open a new one.',
'imageref.limit.title': 'Limit reached',
'imageref.load_error': 'Image could not be loaded: {error}',
'imageref.load_error.title': 'Image error',
'imageref.invalid_file': 'Please use an image file (PNG, JPG, WebP, etc.).',
'imageref.invalid_file.title': 'Not an image',
// Widget manager
'widget.minimize': 'Minimize',
'widget.close': 'Close',
// Data (export/import)
'data.invalid_format': 'Invalid format',
'data.no_boards': 'No valid boards found',
'data.import_confirm': 'Import {count} boards? Existing data will be preserved.',
'data.import_confirm.title': 'JSON Import',
'data.import_success': '{boards} board(s){notes}{calc}{timer} successfully imported.',
'data.import_success.title': 'Import successful',
'data.import_error': 'Import error: {error}',
'data.import_error.title': 'Import failed',
'data.notes_suffix': ' + {count} note(s)',
'data.calc_suffix': ' + Calculator history',
'data.timer_suffix': ' + Timer presets',
// Browser bookmark import
'bm_import.no_access': 'Cannot access browser bookmarks. Make sure the extension has the required permissions.',
'bm_import.title': 'Bookmark import',
'bm_import.no_folders': 'No bookmark folders found.',
'bm_import.modal_title': 'Import browser bookmarks',
'bm_import.info': 'Select the folders to import as boards. Each folder becomes its own board.',
'bm_import.unnamed': 'Unnamed',
'bm_import.link_count': '{count} link(s)',
'bm_import.folder_count': '{count} folder(s)',
'bm_import.empty': 'empty',
'bm_import.select_all': 'Select all',
'bm_import.deselect_all': 'Deselect all',
'bm_import.import_btn': 'Import',
'bm_import.no_selection': 'Please select at least one folder.',
'bm_import.boards_created': '{count} board(s) created',
'bm_import.bookmarks_imported': '{count} bookmarks imported',
'bm_import.duplicates_skipped': '{count} duplicate(s) skipped',
'bm_import.success_title': 'Import complete',
// Storage
'storage.quota_full': 'Storage full! Please delete old boards or the background image to free up space.',
'storage.quota_full.title': 'Storage full',
// App
'app.backup_reminder': 'You haven\'t made a backup in over a week. If you clear browser data, your boards will be lost. Back up now?',
'app.backup_reminder.title': 'Backup reminder',
'app.backup_now': 'Back up now',
'app.backup_later': 'Later',
'app.no_bookmarks': 'No bookmarks found in this file.',
'app.import_title': 'Import',
'app.html_import_success': '{count} board(s) with {total} bookmarks imported.',
'app.import_success_title': 'Import successful',
'app.invalid_url': 'Invalid URL. Please start with https://.',
'app.invalid_url.title': 'Invalid URL',
// Clock
'clock.days.sun': 'Sun',
'clock.days.mon': 'Mon',
'clock.days.tue': 'Tue',
'clock.days.wed': 'Wed',
'clock.days.thu': 'Thu',
'clock.days.fri': 'Fri',
'clock.days.sat': 'Sat',
'clock.months.jan': 'Jan',
'clock.months.feb': 'Feb',
'clock.months.mar': 'Mar',
'clock.months.apr': 'Apr',
'clock.months.may': 'May',
'clock.months.jun': 'Jun',
'clock.months.jul': 'Jul',
'clock.months.aug': 'Aug',
'clock.months.sep': 'Sep',
'clock.months.oct': 'Oct',
'clock.months.nov': 'Nov',
'clock.months.dec': 'Dec',
// Settings
'settings.file_read_error': 'Error reading file. Please choose a different file.',
'settings.file_read_error.title': 'File error',
'settings.reset_confirm': 'Really delete all boards and settings? This cannot be undone.',
'settings.reset_confirm.title': 'Reset everything',
'settings.reset_confirm.button': 'Delete all',
// Header
'header.import': 'Import',
'header.board': 'Board',
'header.note': 'Note',
'header.theme': 'Theme',
'header.settings': 'Settings',
// Settings panel heading
'settings.title': 'Settings',
// Settings panel sections
'settings.section.widgets': 'WIDGETS',
'settings.section.data': 'DATA & HELP',
'settings.section.danger': 'DANGER ZONE',
'settings.section.bg': 'BACKGROUND',
'settings.section.display': 'DISPLAY',
// Settings rows
'settings.language': 'Language',
'settings.language.desc': 'Choose display language',
'settings.language.auto': 'Automatic',
'settings.toolbar_pos': 'Toolbar position',
'settings.toolbar_pos.desc': 'Show widget toolbar on the left or right',
'settings.toolbar_pos.right': 'Right',
'settings.toolbar_pos.left': 'Left',
'settings.image_ref': 'Image reference widgets',
'settings.image_ref.desc': 'Show images as reference (current session only)',
'settings.export': 'Export backup',
'settings.export.desc': 'Save all boards, notes and settings as JSON',
'settings.export.btn': 'Export',
'settings.import': 'Import backup',
'settings.import.desc': 'Restore a JSON backup',
'settings.browser_import': 'Browser bookmarks',
'settings.browser_import.desc': 'Import bookmarks directly from the browser',
'settings.onboarding': 'Replay onboarding',
'settings.onboarding.desc': 'Show the welcome tour again',
'settings.reset': 'Reset everything',
'settings.reset.desc': 'Deletes all boards, notes and settings',
'settings.compact': 'Compact mode',
'settings.compact.desc': 'Less spacing for more bookmarks',
'settings.shorten': 'Shorten long titles',
'settings.shorten.desc': 'Truncate titles to one line with "…"',
'settings.search': 'Show search bar',
'settings.search.desc': 'Toggle search bar below the header',
'settings.newtab': 'Links in new tab',
'settings.newtab.desc': 'Open bookmarks in a new browser tab',
'settings.showdesc': 'Show descriptions',
'settings.showdesc.desc': 'Show saved description below bookmarks',
'settings.hideextra': 'Hide bookmarks',
'settings.hideextra.desc': 'Hide excess bookmarks in long boards',
'settings.visible_count': 'Visible bookmarks',
'settings.visible_count.desc': 'Number before hiding',
'settings.bg_url': 'Image URL',
'settings.bg_url.desc': 'Custom background image via URL',
'settings.bg_change': 'Change',
'settings.bg_apply': 'Apply',
'settings.bg_upload': 'Upload file',
'settings.bg_upload.desc': 'Use a local image as background',
'settings.search_engine_toggle': 'Switch search engine',
// Modals
'modal.new_board': 'New Board',
'modal.board_name': 'Board name...',
'modal.create': 'Create',
'modal.new_bookmark': 'New Bookmark',
'modal.bm_title': 'Title...',
'modal.bm_desc': 'Description (optional)',
'modal.bm_add': 'Add',
'modal.rename': 'Rename',
'modal.rename_placeholder': 'New name...',
'modal.rename_confirm': 'Rename',
'modal.theme_header': 'Theme',
// About
'about.title': '⬡ HELLION NEWTAB',
'about.impressum': 'Legal Notice',
'about.developer': 'Developer',
'about.company': 'Company',
'about.license': 'License',
'about.storage': 'Data storage',
'about.storage.value': '100% local · No server · No account',
'about.bugreport': 'Bug Report / Feedback',
'about.support': 'Support',
'about.browsers': 'Compatible browsers',
// Notebook
'notebook.title': 'Notebook',
// Search
'search.placeholder': 'Search the web…',
// Widget toolbar tooltips
'toolbar.note': 'Create note',
'toolbar.checklist': 'Create checklist',
'toolbar.calculator': 'Calculator',
'toolbar.timer': 'Timer',
'toolbar.imageref': 'Image reference',
'toolbar.notebook': 'All notes'
}
};
/** @type {string} Aktuell aktive Sprache */
let currentLang = 'de';
/**
* Übersetzungsstring abrufen mit optionalen Platzhaltern
* @param {string} key - Schlüssel im STRINGS-Objekt
* @param {Object} [vars] - Platzhalter-Werte (z.B. { max: 5 })
* @returns {string}
*/
function t(key, vars) {
let str = (STRINGS[currentLang] && STRINGS[currentLang][key])
|| (STRINGS['en'] && STRINGS['en'][key])
|| key;
if (vars) {
for (const [k, v] of Object.entries(vars)) {
str = str.replaceAll('{' + k + '}', v);
}
}
return str;
}
/**
* Alle data-i18n Elemente im Dokument mit aktueller Sprache befüllen
*/
function applyLanguage() {
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.dataset.i18n);
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.placeholder = t(el.dataset.i18nPlaceholder);
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = t(el.dataset.i18nTitle);
});
}
/**
* 'auto' auflösen zu konkretem Sprachcode
* @param {string} lang - 'de', 'en' oder 'auto'
* @returns {string} 'de' oder 'en'
*/
function resolveLang(lang) {
return (lang === 'auto')
? (navigator.language.startsWith('de') ? 'de' : 'en')
: lang;
}
/**
* Sprache setzen, speichern und DOM aktualisieren
* @param {string} lang - 'de', 'en' oder 'auto'
*/
function setLanguage(lang) {
currentLang = resolveLang(lang);
document.documentElement.lang = currentLang;
applyLanguage();
}
/**
* i18n-Modul — öffentliche API
*/
const I18n = {
/** Aktuell aktive Sprache (nach Auto-Auflösung) */
get currentLang() { return currentLang; },
/**
* Initialisierung: Sprache aus Settings lesen, auflösen, DOM anwenden
* Muss NACH dem Laden des settings-Objekts aufgerufen werden
*/
init() {
const lang = (typeof settings !== 'undefined' && settings.language)
? settings.language
: 'auto';
currentLang = resolveLang(lang);
document.documentElement.lang = currentLang;
applyLanguage();
}
};
+22 -22
View File
@@ -114,8 +114,8 @@ const ImageRef = {
} catch (e) { } catch (e) {
console.warn('ImageRef: sessionStorage Write fehlgeschlagen', e); console.warn('ImageRef: sessionStorage Write fehlgeschlagen', e);
HellionDialog.alert( HellionDialog.alert(
'Bild konnte nicht gespeichert werden. Der Browser-Speicher ist voll.', t('imageref.storage_error'),
{ type: 'danger', title: 'Speicherfehler' } { type: 'danger', title: t('imageref.storage_error.title') }
); );
} }
}, },
@@ -144,8 +144,8 @@ const ImageRef = {
if (this._images.length >= this.MAX_IMAGES) { if (this._images.length >= this.MAX_IMAGES) {
await HellionDialog.alert( await HellionDialog.alert(
'Maximal ' + this.MAX_IMAGES + ' Bild-Widgets gleichzeitig. Schliesse eines um ein neues zu oeffnen.', t('imageref.limit', { max: this.MAX_IMAGES }),
{ type: 'warning', title: 'Limit erreicht' } { type: 'warning', title: t('imageref.limit.title') }
); );
return; return;
} }
@@ -172,8 +172,8 @@ const ImageRef = {
dataUrl = await this._processFile(file); dataUrl = await this._processFile(file);
} catch (err) { } catch (err) {
await HellionDialog.alert( await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message, t('imageref.load_error', { error: err.message }),
{ type: 'danger', title: 'Bildfehler' } { type: 'danger', title: t('imageref.load_error.title') }
); );
return; return;
} }
@@ -206,7 +206,7 @@ const ImageRef = {
_createWidget(imageData, dataUrl) { _createWidget(imageData, dataUrl) {
WidgetManager.create('image', { WidgetManager.create('image', {
id: imageData.id, id: imageData.id,
title: imageData.label || 'Bild-Referenz', title: imageData.label || t('imageref.title'),
x: imageData.x, x: imageData.x,
y: imageData.y, y: imageData.y,
width: imageData.width, width: imageData.width,
@@ -249,14 +249,14 @@ const ImageRef = {
const img = document.createElement('img'); const img = document.createElement('img');
img.className = 'imgref-img'; img.className = 'imgref-img';
img.src = dataUrl; img.src = dataUrl;
img.alt = imageData.label || 'Bild-Referenz'; img.alt = imageData.label || t('imageref.title');
wrapper.appendChild(img); wrapper.appendChild(img);
// Bild ersetzen Button // Bild ersetzen Button
const replaceBtn = document.createElement('button'); const replaceBtn = document.createElement('button');
replaceBtn.className = 'imgref-replace-btn'; replaceBtn.className = 'imgref-replace-btn';
replaceBtn.type = 'button'; replaceBtn.type = 'button';
replaceBtn.textContent = 'Bild ersetzen'; replaceBtn.textContent = t('imageref.replace');
replaceBtn.addEventListener('click', async () => { replaceBtn.addEventListener('click', async () => {
const file = await this._pickFile(); const file = await this._pickFile();
if (!file) return; if (!file) return;
@@ -266,8 +266,8 @@ const ImageRef = {
this.renderBody(imageData, bodyEl, newDataUrl); this.renderBody(imageData, bodyEl, newDataUrl);
} catch (err) { } catch (err) {
await HellionDialog.alert( await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message, t('imageref.load_error', { error: err.message }),
{ type: 'danger', title: 'Bildfehler' } { type: 'danger', title: t('imageref.load_error.title') }
); );
} }
}); });
@@ -283,7 +283,7 @@ const ImageRef = {
const label = document.createElement('input'); const label = document.createElement('input');
label.className = 'imgref-label'; label.className = 'imgref-label';
label.type = 'text'; label.type = 'text';
label.placeholder = 'Beschriftung (optional)'; label.placeholder = t('imageref.label_placeholder');
label.maxLength = 100; label.maxLength = 100;
label.value = imageData.label || ''; label.value = imageData.label || '';
@@ -295,8 +295,8 @@ const ImageRef = {
const entry = WidgetManager._widgets.get(imageData.id); const entry = WidgetManager._widgets.get(imageData.id);
if (entry) { if (entry) {
const titleEl = entry.el.querySelector('.widget-title-text'); const titleEl = entry.el.querySelector('.widget-title-text');
if (titleEl) titleEl.textContent = text || 'Bild-Referenz'; if (titleEl) titleEl.textContent = text || t('imageref.title');
entry.state.title = text || 'Bild-Referenz'; entry.state.title = text || t('imageref.title');
} }
this._debouncedSave(); this._debouncedSave();
@@ -321,7 +321,7 @@ const ImageRef = {
icon.textContent = '\uD83D\uDDBC\uFE0F'; icon.textContent = '\uD83D\uDDBC\uFE0F';
const text = document.createElement('span'); const text = document.createElement('span');
text.textContent = 'Klicken oder Bild hierher ziehen'; text.textContent = t('imageref.dropzone');
dropzone.append(icon, text); dropzone.append(icon, text);
@@ -336,8 +336,8 @@ const ImageRef = {
await this.save(); await this.save();
} catch (err) { } catch (err) {
await HellionDialog.alert( await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message, t('imageref.load_error', { error: err.message }),
{ type: 'danger', title: 'Bildfehler' } { type: 'danger', title: t('imageref.load_error.title') }
); );
} }
}); });
@@ -363,8 +363,8 @@ const ImageRef = {
const file = e.dataTransfer.files[0]; const file = e.dataTransfer.files[0];
if (!file || !file.type.startsWith('image/')) { if (!file || !file.type.startsWith('image/')) {
await HellionDialog.alert( await HellionDialog.alert(
'Bitte eine Bilddatei verwenden (PNG, JPG, WebP, etc.).', t('imageref.invalid_file'),
{ type: 'warning', title: 'Kein Bild' } { type: 'warning', title: t('imageref.invalid_file.title') }
); );
return; return;
} }
@@ -376,8 +376,8 @@ const ImageRef = {
await this.save(); await this.save();
} catch (err) { } catch (err) {
await HellionDialog.alert( await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message, t('imageref.load_error', { error: err.message }),
{ type: 'danger', title: 'Bildfehler' } { type: 'danger', title: t('imageref.load_error.title') }
); );
} }
}); });
@@ -433,7 +433,7 @@ const ImageRef = {
img.onerror = () => { img.onerror = () => {
URL.revokeObjectURL(objectUrl); URL.revokeObjectURL(objectUrl);
reject(new Error('Bild konnte nicht geladen werden')); reject(new Error(t('imageref.load_error', { error: 'unknown' })));
}; };
img.src = objectUrl; img.src = objectUrl;
+16 -16
View File
@@ -75,15 +75,15 @@ const Notes = {
async create(template) { async create(template) {
if (this._notes.length >= this.MAX_NOTES) { if (this._notes.length >= this.MAX_NOTES) {
await HellionDialog.alert( await HellionDialog.alert(
'Maximale Anzahl erreicht! Du kannst maximal ' + this.MAX_NOTES + ' Notes gleichzeitig haben. Lösche eine bestehende Note um eine neue zu erstellen.', t('notes.limit_message', { max: this.MAX_NOTES }),
{ type: 'warning', title: 'Limit erreicht' } { type: 'warning', title: t('notes.limit_title') }
); );
return null; return null;
} }
const noteData = { const noteData = {
id: 'note_' + uid(), id: 'note_' + uid(),
title: template === 'checklist' ? 'Checkliste' : 'Note', title: template === 'checklist' ? t('notes.checklist_title') : t('notes.default_title'),
content: '', content: '',
template: template, template: template,
x: 120 + (this._notes.length * 30), x: 120 + (this._notes.length * 30),
@@ -138,7 +138,7 @@ const Notes = {
_renderTextBody(noteData, bodyEl) { _renderTextBody(noteData, bodyEl) {
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
textarea.className = 'widget-textarea'; textarea.className = 'widget-textarea';
textarea.placeholder = 'Notiz schreiben...'; textarea.placeholder = t('notes.placeholder');
textarea.spellcheck = false; textarea.spellcheck = false;
textarea.value = noteData.content || ''; textarea.value = noteData.content || '';
textarea.maxLength = this.MAX_CHARS; textarea.maxLength = this.MAX_CHARS;
@@ -204,7 +204,7 @@ const Notes = {
const addInput = document.createElement('input'); const addInput = document.createElement('input');
addInput.className = 'checklist-add-input'; addInput.className = 'checklist-add-input';
addInput.type = 'text'; addInput.type = 'text';
addInput.placeholder = 'Neues Item...'; addInput.placeholder = t('notes.checklist_placeholder');
addInput.maxLength = 100; addInput.maxLength = 100;
addInput.addEventListener('keydown', async (e) => { addInput.addEventListener('keydown', async (e) => {
@@ -276,11 +276,11 @@ const Notes = {
// Auto-Titel: "X/Y erledigt" falls kein manueller Titel // Auto-Titel: "X/Y erledigt" falls kein manueller Titel
const widgetEntry = WidgetManager._widgets.get(noteData.id); const widgetEntry = WidgetManager._widgets.get(noteData.id);
if (widgetEntry) { if (widgetEntry) {
const defaultTitle = done + '/' + total + ' erledigt'; const defaultTitle = t('notes.checklist_progress', { done: done, total: total });
const titleEl = widgetEntry.el.querySelector('.widget-title'); const titleEl = widgetEntry.el.querySelector('.widget-title');
if (titleEl && titleEl.contentEditable !== 'true') { if (titleEl && titleEl.contentEditable !== 'true') {
// Nur wenn Titel noch Standard ist // Nur wenn Titel noch Standard ist
if (noteData.title === 'Checkliste' || /^\d+\/\d+ erledigt$/.test(noteData.title)) { if (noteData.title === t('notes.checklist_title') || /^\d+\/\d+\s/.test(noteData.title)) {
noteData.title = defaultTitle; noteData.title = defaultTitle;
titleEl.textContent = defaultTitle; titleEl.textContent = defaultTitle;
widgetEntry.state.title = defaultTitle; widgetEntry.state.title = defaultTitle;
@@ -307,8 +307,8 @@ const Notes = {
if (idx === -1) return; if (idx === -1) return;
const ok = await HellionDialog.confirm( const ok = await HellionDialog.confirm(
'Note endgültig löschen? Das kann nicht rückgängig gemacht werden.', t('notes.delete_confirm'),
{ type: 'danger', title: 'Note löschen', confirmText: 'Löschen' } { type: 'danger', title: t('notes.delete_title'), confirmText: t('notes.delete_button') }
); );
if (!ok) return; if (!ok) return;
@@ -330,7 +330,7 @@ const Notes = {
} else { } else {
md += noteData.content || ''; md += noteData.content || '';
} }
md += '\n\n---\n*Exportiert aus Hellion Dashboard*\n'; md += '\n\n---\n*' + t('notes.export_footer') + '*\n';
const blob = new Blob([md], { type: 'text/markdown' }); const blob = new Blob([md], { type: 'text/markdown' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -419,9 +419,9 @@ const Notes = {
if (note.template === 'checklist') { if (note.template === 'checklist') {
const total = note.checklistItems ? note.checklistItems.length : 0; const total = note.checklistItems ? note.checklistItems.length : 0;
const done = note.checklistItems ? note.checklistItems.filter(i => i.checked).length : 0; const done = note.checklistItems ? note.checklistItems.filter(i => i.checked).length : 0;
preview.textContent = done + '/' + total + ' erledigt'; preview.textContent = t('notes.checklist_progress', { done: done, total: total });
} else { } else {
preview.textContent = (note.content || '').slice(0, 50) || 'Leer'; preview.textContent = (note.content || '').slice(0, 50) || t('notes.empty_preview');
} }
// Actions // Actions
@@ -430,7 +430,7 @@ const Notes = {
const btnExport = document.createElement('button'); const btnExport = document.createElement('button');
btnExport.className = 'notebook-slot-btn'; btnExport.className = 'notebook-slot-btn';
btnExport.textContent = 'Export'; btnExport.textContent = t('notes.export');
btnExport.addEventListener('click', (e) => { btnExport.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
this.exportNote(note); this.exportNote(note);
@@ -470,7 +470,7 @@ const Notes = {
slot.className = 'notebook-slot-empty'; slot.className = 'notebook-slot-empty';
const label = document.createElement('span'); const label = document.createElement('span');
label.textContent = '+ Note erstellen'; label.textContent = t('notes.create');
slot.appendChild(label); slot.appendChild(label);
// Klick zeigt Typ-Auswahl // Klick zeigt Typ-Auswahl
@@ -485,7 +485,7 @@ const Notes = {
const btnText = document.createElement('button'); const btnText = document.createElement('button');
btnText.className = 'notebook-type-btn'; btnText.className = 'notebook-type-btn';
btnText.textContent = '\u270E Freitext'; btnText.textContent = t('notes.text_type');
btnText.addEventListener('click', async (e) => { btnText.addEventListener('click', async (e) => {
e.stopPropagation(); e.stopPropagation();
await this.create('text'); await this.create('text');
@@ -494,7 +494,7 @@ const Notes = {
const btnCheck = document.createElement('button'); const btnCheck = document.createElement('button');
btnCheck.className = 'notebook-type-btn'; btnCheck.className = 'notebook-type-btn';
btnCheck.textContent = '\u2611 Checkliste'; btnCheck.textContent = t('notes.checklist_type');
btnCheck.addEventListener('click', async (e) => { btnCheck.addEventListener('click', async (e) => {
e.stopPropagation(); e.stopPropagation();
await this.create('checklist'); await this.create('checklist');
+26 -38
View File
@@ -9,52 +9,40 @@ const Onboarding = {
slides: [ slides: [
{ {
hero: '\u2B21', hero: '\u2B21',
title: 'Willkommen bei Hellion Dashboard', titleKey: 'onboarding.s1.title',
text: 'Dein neuer Browser-Startbildschirm. Minimalistisch, schnell und vollst\u00E4ndig lokal \u2014 keine Cloud, kein Account, keine Datensammlung.' textKey: 'onboarding.s1.text'
}, },
{ {
hero: '\uD83D\uDCCB', hero: '\uD83D\uDCCB',
title: 'Boards & Bookmarks', titleKey: 'onboarding.s2.title',
features: [ featureKeys: ['onboarding.s2.f1', 'onboarding.s2.f2', 'onboarding.s2.f3', 'onboarding.s2.f4']
'Erstelle Boards mit dem \u201E+ Board\u201C Button oben',
'Importiere Browser-Lesezeichen \u00FCber den \u201EImport\u201C Button im Header',
'Drag & Drop zum Umsortieren von Boards und Links',
'Blur-Modus f\u00FCr private Boards (\uD83D\uDD12 Icon)'
]
}, },
{ {
hero: '\uD83C\uDFA8', hero: '\uD83C\uDFA8',
title: '11 handgefertigte Themes', titleKey: 'onboarding.s3.title',
text: 'Klicke auf den \u201ETheme\u201C Button im Header um dein Theme zu w\u00E4hlen. Jedes hat seinen eigenen Stil und Farbpalette.', textKey: 'onboarding.s3.text',
showThemes: true showThemes: true
}, },
{ {
hero: '\uD83E\uDDF0', hero: '\uD83E\uDDF0',
title: 'Widget-Toolbar', titleKey: 'onboarding.s4.title',
features: [ featureKeys: ['onboarding.s4.f1', 'onboarding.s4.f2', 'onboarding.s4.f3', 'onboarding.s4.f4', 'onboarding.s4.f5', 'onboarding.s4.f6']
'Die schwebenden Buttons rechts \u00F6ffnen Widgets',
'Notes und Checklisten f\u00FCr schnelle Notizen',
'Taschenrechner mit History',
'Timer/Countdown mit speicherbaren Presets',
'Bild-Referenz Widgets (aktivierbar in Settings)',
'Notebook-Sidebar zeigt alle Notes auf einen Blick'
]
}, },
{ {
hero: '\uD83D\uDEE1\uFE0F', hero: '\uD83D\uDEE1\uFE0F',
title: 'Backups nicht vergessen!', titleKey: 'onboarding.s5.title',
text: 'Deine Daten sind lokal im Browser gespeichert. Wenn du Browserdaten l\u00F6schst, gehen sie verloren! Sichere regelm\u00E4\u00DFig \u00FCber Settings \u2192 Data \u2192 Export. Wir erinnern dich alle 7 Tage daran.' textKey: 'onboarding.s5.text'
}, },
{ {
hero: '\uD83C\uDFAE', hero: '\uD83C\uDFAE',
title: 'Gaming Starter Board', titleKey: 'onboarding.s6.title',
text: 'Spielst du Games wie Satisfactory, Factorio oder Star Citizen? Ich kann ein Board mit n\u00FCtzlichen Community-Links anlegen.', textKey: 'onboarding.s6.text',
interactive: 'gaming-board' interactive: 'gaming-board'
}, },
{ {
hero: '\uD83D\uDE80', hero: '\uD83D\uDE80',
title: 'Bereit!', titleKey: 'onboarding.s7.title',
text: 'Erstelle dein erstes Board mit \u201E+ Board\u201C oder importiere deine Browser-Lesezeichen \u00FCber den Import-Button im Header. Viel Spa\u00DF!' textKey: 'onboarding.s7.text'
} }
], ],
@@ -87,7 +75,7 @@ const Onboarding = {
if (!isLast) { if (!isLast) {
const skip = document.createElement('button'); const skip = document.createElement('button');
skip.className = 'onboarding-skip'; skip.className = 'onboarding-skip';
skip.textContent = '\u00DCberspringen'; skip.textContent = t('onboarding.skip');
skip.addEventListener('click', () => this._finish()); skip.addEventListener('click', () => this._finish());
modal.appendChild(skip); modal.appendChild(skip);
} }
@@ -103,22 +91,22 @@ const Onboarding = {
const title = document.createElement('div'); const title = document.createElement('div');
title.className = 'onboarding-title'; title.className = 'onboarding-title';
title.textContent = slide.title; title.textContent = t(slide.titleKey);
slideEl.appendChild(title); slideEl.appendChild(title);
if (slide.text) { if (slide.textKey) {
const text = document.createElement('div'); const text = document.createElement('div');
text.className = 'onboarding-text'; text.className = 'onboarding-text';
text.textContent = slide.text; text.textContent = t(slide.textKey);
slideEl.appendChild(text); slideEl.appendChild(text);
} }
if (slide.features) { if (slide.featureKeys) {
const list = document.createElement('ul'); const list = document.createElement('ul');
list.className = 'onboarding-feature-list'; list.className = 'onboarding-feature-list';
slide.features.forEach(f => { slide.featureKeys.forEach(key => {
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = f; li.textContent = t(key);
list.appendChild(li); list.appendChild(li);
}); });
slideEl.appendChild(list); slideEl.appendChild(list);
@@ -160,7 +148,7 @@ const Onboarding = {
if (this.currentSlide > 0) { if (this.currentSlide > 0) {
const backBtn = document.createElement('button'); const backBtn = document.createElement('button');
backBtn.className = 'btn-secondary'; backBtn.className = 'btn-secondary';
backBtn.textContent = 'Zur\u00FCck'; backBtn.textContent = t('onboarding.back');
backBtn.addEventListener('click', () => { backBtn.addEventListener('click', () => {
this.currentSlide--; this.currentSlide--;
this._render(); this._render();
@@ -172,7 +160,7 @@ const Onboarding = {
// Interaktive Slide: Zwei Buttons statt "Weiter" // Interaktive Slide: Zwei Buttons statt "Weiter"
const noBtn = document.createElement('button'); const noBtn = document.createElement('button');
noBtn.className = 'btn-secondary'; noBtn.className = 'btn-secondary';
noBtn.textContent = 'Nein danke'; noBtn.textContent = t('onboarding.no');
noBtn.addEventListener('click', () => { noBtn.addEventListener('click', () => {
this.currentSlide++; this.currentSlide++;
this._render(); this._render();
@@ -180,7 +168,7 @@ const Onboarding = {
const yesBtn = document.createElement('button'); const yesBtn = document.createElement('button');
yesBtn.className = 'btn-primary'; yesBtn.className = 'btn-primary';
yesBtn.textContent = 'Ja, gerne'; yesBtn.textContent = t('onboarding.yes');
yesBtn.addEventListener('click', async () => { yesBtn.addEventListener('click', async () => {
await this._createGamingBoard(); await this._createGamingBoard();
this.currentSlide++; this.currentSlide++;
@@ -191,13 +179,13 @@ const Onboarding = {
} else if (isLast) { } else if (isLast) {
const startBtn = document.createElement('button'); const startBtn = document.createElement('button');
startBtn.className = 'btn-primary'; startBtn.className = 'btn-primary';
startBtn.textContent = 'Los geht\u2019s!'; startBtn.textContent = t('onboarding.start');
startBtn.addEventListener('click', () => this._finish()); startBtn.addEventListener('click', () => this._finish());
nav.appendChild(startBtn); nav.appendChild(startBtn);
} else { } else {
const nextBtn = document.createElement('button'); const nextBtn = document.createElement('button');
nextBtn.className = 'btn-primary'; nextBtn.className = 'btn-primary';
nextBtn.textContent = 'Weiter'; nextBtn.textContent = t('onboarding.next');
nextBtn.addEventListener('click', () => { nextBtn.addEventListener('click', () => {
this.currentSlide++; this.currentSlide++;
this._render(); this._render();
+20 -4
View File
@@ -83,6 +83,10 @@ function applySettings() {
const toolbarPosEl = document.getElementById('settingToolbarPos'); const toolbarPosEl = document.getElementById('settingToolbarPos');
if (toolbarPosEl) toolbarPosEl.value = settings.toolbarPos || 'right'; if (toolbarPosEl) toolbarPosEl.value = settings.toolbarPos || 'right';
// Sprache (Dropdown-Wert setzen — I18n.init() übernimmt die eigentliche Anwendung)
const langEl = document.getElementById('settingLanguage');
if (langEl) langEl.value = settings.language || 'auto';
applyTheme(settings.theme || 'nebula', !!settings.bgUrl); applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
if (settings.bgUrl) { if (settings.bgUrl) {
@@ -184,11 +188,22 @@ function bindSettingsEvents() {
await saveSettings(); await saveSettings();
}; };
reader.onerror = () => { reader.onerror = () => {
HellionDialog.alert('Fehler beim Lesen der Datei. Bitte eine andere Datei wählen.', { type: 'danger', title: 'Dateifehler' }); HellionDialog.alert(t('settings.file_read_error'), { type: 'danger', title: t('settings.file_read_error.title') });
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
// Sprach-Einstellung
const languageEl = document.getElementById('settingLanguage');
if (languageEl) {
languageEl.value = settings.language || 'auto';
languageEl.addEventListener('change', async (e) => {
settings.language = e.target.value;
setLanguage(e.target.value);
await saveSettings();
});
}
// Toolbar-Position Setting // Toolbar-Position Setting
const toolbarPosEl = document.getElementById('settingToolbarPos'); const toolbarPosEl = document.getElementById('settingToolbarPos');
if (toolbarPosEl) { if (toolbarPosEl) {
@@ -209,17 +224,18 @@ function bindSettingsEvents() {
// Reset All // Reset All
document.getElementById('btnResetAll').addEventListener('click', async () => { document.getElementById('btnResetAll').addEventListener('click', async () => {
const ok = await HellionDialog.confirm( const ok = await HellionDialog.confirm(
'Wirklich alle Boards und Einstellungen löschen? Das kann nicht rückgängig gemacht werden.', t('settings.reset_confirm'),
{ type: 'danger', title: 'Alles zurücksetzen', confirmText: 'Alles löschen' } { type: 'danger', title: t('settings.reset_confirm.title'), confirmText: t('settings.reset_confirm.button') }
); );
if (!ok) return; if (!ok) return;
boards = []; boards = [];
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false, settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula', hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
showSearch: true, searchEngine: 'google', toolbarPos: 'right', showSearch: true, searchEngine: 'google', toolbarPos: 'right',
imageRefEnabled: false }; imageRefEnabled: false, language: 'auto' };
await saveBoards(); await saveBoards();
await saveSettings(); await saveSettings();
setLanguage('auto');
applySettings(); applySettings();
renderBoards(); renderBoards();
closeSettings(); closeSettings();
+2 -1
View File
@@ -17,7 +17,8 @@ let settings = {
showSearch: true, showSearch: true,
searchEngine: 'google', searchEngine: 'google',
toolbarPos: 'right', toolbarPos: 'right',
imageRefEnabled: false imageRefEnabled: false,
language: 'auto'
}; };
function uid() { function uid() {
+2 -2
View File
@@ -23,7 +23,7 @@ const Store = {
chrome.storage.local.set({ [key]: value }, () => { chrome.storage.local.set({ [key]: value }, () => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
console.error('Storage-Fehler:', chrome.runtime.lastError.message); console.error('Storage-Fehler:', chrome.runtime.lastError.message);
HellionDialog.alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.', { type: 'danger', title: 'Speicher voll' }); HellionDialog.alert(t('storage.quota_full'), { type: 'danger', title: t('storage.quota_full.title') });
reject(new Error(chrome.runtime.lastError.message)); reject(new Error(chrome.runtime.lastError.message));
return; return;
} }
@@ -35,7 +35,7 @@ const Store = {
resolve(); resolve();
} catch (e) { } catch (e) {
console.error('Storage-Fehler:', e.message); console.error('Storage-Fehler:', e.message);
HellionDialog.alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.', { type: 'danger', title: 'Speicher voll' }); HellionDialog.alert(t('storage.quota_full'), { type: 'danger', title: t('storage.quota_full.title') });
reject(e); reject(e);
} }
} }
+17 -17
View File
@@ -82,7 +82,7 @@ const Timer = {
WidgetManager.create('timer', { WidgetManager.create('timer', {
id: this.WIDGET_ID, id: this.WIDGET_ID,
title: 'Timer', title: t('timer.title'),
x: saved.x || 600, x: saved.x || 600,
y: saved.y || 80, y: saved.y || 80,
width: saved.width || 260, width: saved.width || 260,
@@ -190,7 +190,7 @@ const Timer = {
const btnStart = document.createElement('button'); const btnStart = document.createElement('button');
btnStart.className = 'timer-ctrl-btn primary'; btnStart.className = 'timer-ctrl-btn primary';
btnStart.type = 'button'; btnStart.type = 'button';
btnStart.textContent = 'Start'; btnStart.textContent = t('timer.start');
btnStart.addEventListener('click', () => { btnStart.addEventListener('click', () => {
if (!this._running && this._remaining === 0) { if (!this._running && this._remaining === 0) {
this._applyInput(); this._applyInput();
@@ -202,7 +202,7 @@ const Timer = {
const btnPause = document.createElement('button'); const btnPause = document.createElement('button');
btnPause.className = 'timer-ctrl-btn'; btnPause.className = 'timer-ctrl-btn';
btnPause.type = 'button'; btnPause.type = 'button';
btnPause.textContent = 'Pause'; btnPause.textContent = t('timer.pause');
btnPause.disabled = true; btnPause.disabled = true;
btnPause.addEventListener('click', () => this._pause()); btnPause.addEventListener('click', () => this._pause());
this._btnPause = btnPause; this._btnPause = btnPause;
@@ -210,7 +210,7 @@ const Timer = {
const btnReset = document.createElement('button'); const btnReset = document.createElement('button');
btnReset.className = 'timer-ctrl-btn danger'; btnReset.className = 'timer-ctrl-btn danger';
btnReset.type = 'button'; btnReset.type = 'button';
btnReset.textContent = 'Reset'; btnReset.textContent = t('timer.reset');
btnReset.addEventListener('click', () => this._reset()); btnReset.addEventListener('click', () => this._reset());
this._btnReset = btnReset; this._btnReset = btnReset;
@@ -253,13 +253,13 @@ const Timer = {
const title = document.createElement('span'); const title = document.createElement('span');
title.className = 'timer-presets-title'; title.className = 'timer-presets-title';
title.textContent = 'Presets'; title.textContent = t('timer.presets');
const addBtn = document.createElement('button'); const addBtn = document.createElement('button');
addBtn.className = 'timer-preset-add'; addBtn.className = 'timer-preset-add';
addBtn.type = 'button'; addBtn.type = 'button';
addBtn.textContent = '+'; addBtn.textContent = '+';
addBtn.title = 'Preset speichern'; addBtn.title = t('timer.save_preset');
addBtn.addEventListener('click', () => this._showAddPreset(container)); addBtn.addEventListener('click', () => this._showAddPreset(container));
header.append(title, addBtn); header.append(title, addBtn);
@@ -322,8 +322,8 @@ const Timer = {
if (this._presets.length >= this.MAX_PRESETS) { if (this._presets.length >= this.MAX_PRESETS) {
HellionDialog.alert( HellionDialog.alert(
'Maximale Anzahl erreicht! Du kannst maximal ' + this.MAX_PRESETS + ' Presets speichern.', t('timer.limit_message', { max: this.MAX_PRESETS }),
{ type: 'warning', title: 'Limit erreicht' } { type: 'warning', title: t('timer.limit_title') }
); );
return; return;
} }
@@ -334,8 +334,8 @@ const Timer = {
const parsed = this._parseTimeInput(this._inputEl.value); const parsed = this._parseTimeInput(this._inputEl.value);
if (parsed === 0) { if (parsed === 0) {
HellionDialog.alert( HellionDialog.alert(
'Gib zuerst eine Zeit ein, bevor du ein Preset speicherst.', t('timer.no_time_message'),
{ type: 'info', title: 'Keine Zeit' } { type: 'info', title: t('timer.no_time_title') }
); );
return; return;
} }
@@ -347,13 +347,13 @@ const Timer = {
const nameInput = document.createElement('input'); const nameInput = document.createElement('input');
nameInput.className = 'timer-add-input'; nameInput.className = 'timer-add-input';
nameInput.type = 'text'; nameInput.type = 'text';
nameInput.placeholder = 'Name...'; nameInput.placeholder = t('timer.preset_name_placeholder');
nameInput.maxLength = 20; nameInput.maxLength = 20;
const confirmBtn = document.createElement('button'); const confirmBtn = document.createElement('button');
confirmBtn.className = 'timer-add-confirm'; confirmBtn.className = 'timer-add-confirm';
confirmBtn.type = 'button'; confirmBtn.type = 'button';
confirmBtn.textContent = 'OK'; confirmBtn.textContent = t('timer.ok');
const doAdd = async () => { const doAdd = async () => {
const name = nameInput.value.trim(); const name = nameInput.value.trim();
@@ -508,9 +508,9 @@ const Timer = {
_startTitleBlink() { _startTitleBlink() {
this._originalTitle = document.title; this._originalTitle = document.title;
this._blinkIntervalId = setInterval(() => { this._blinkIntervalId = setInterval(() => {
document.title = document.title === '[!] Timer abgelaufen' document.title = document.title === t('timer.finished_title')
? this._originalTitle ? this._originalTitle
: '[!] Timer abgelaufen'; : t('timer.finished_title');
}, 1000); }, 1000);
}, },
@@ -521,7 +521,7 @@ const Timer = {
if (this._blinkIntervalId) { if (this._blinkIntervalId) {
clearInterval(this._blinkIntervalId); clearInterval(this._blinkIntervalId);
this._blinkIntervalId = null; this._blinkIntervalId = null;
document.title = this._originalTitle || 'Hellion Dashboard'; document.title = this._originalTitle || t('timer.default_page_title');
} }
this._finished = false; this._finished = false;
this._updateDisplay(); this._updateDisplay();
@@ -534,7 +534,7 @@ const Timer = {
_updateMuteBtn() { _updateMuteBtn() {
if (!this._muteBtn) return; if (!this._muteBtn) return;
this._muteBtn.textContent = this._muted ? '\uD83D\uDD07' : '\uD83D\uDD0A'; this._muteBtn.textContent = this._muted ? '\uD83D\uDD07' : '\uD83D\uDD0A';
this._muteBtn.title = this._muted ? 'Ton einschalten' : 'Ton ausschalten'; this._muteBtn.title = this._muted ? t('timer.unmute') : t('timer.mute');
this._muteBtn.classList.toggle('muted', this._muted); this._muteBtn.classList.toggle('muted', this._muted);
}, },
@@ -555,7 +555,7 @@ const Timer = {
_updateControls() { _updateControls() {
if (this._btnStart) { if (this._btnStart) {
this._btnStart.disabled = this._running; this._btnStart.disabled = this._running;
this._btnStart.textContent = this._finished ? 'Neustart' : 'Start'; this._btnStart.textContent = this._finished ? t('timer.restart') : t('timer.start');
} }
if (this._btnPause) { if (this._btnPause) {
this._btnPause.disabled = !this._running; this._btnPause.disabled = !this._running;
+4 -4
View File
@@ -20,7 +20,7 @@ const WidgetManager = {
const state = { const state = {
id, id,
type, type,
title: config.title || 'Note', title: config.title || t('notes.default_title'),
x: config.x || 120, x: config.x || 120,
y: config.y || 80, y: config.y || 80,
width: config.width || 280, width: config.width || 280,
@@ -75,7 +75,7 @@ const WidgetManager = {
title.addEventListener('blur', async () => { title.addEventListener('blur', async () => {
title.contentEditable = 'false'; title.contentEditable = 'false';
const newTitle = title.textContent.trim().slice(0, 20); const newTitle = title.textContent.trim().slice(0, 20);
title.textContent = newTitle || 'Note'; title.textContent = newTitle || t('notes.default_title');
const entry = this._widgets.get(state.id); const entry = this._widgets.get(state.id);
if (entry) { if (entry) {
entry.state.title = title.textContent; entry.state.title = title.textContent;
@@ -94,13 +94,13 @@ const WidgetManager = {
const btnMin = document.createElement('button'); const btnMin = document.createElement('button');
btnMin.className = 'widget-btn widget-minimize'; btnMin.className = 'widget-btn widget-minimize';
btnMin.title = 'Minimieren'; btnMin.title = t('widget.minimize');
btnMin.textContent = '\u2500'; btnMin.textContent = '\u2500';
btnMin.addEventListener('click', () => this.minimize(state.id)); btnMin.addEventListener('click', () => this.minimize(state.id));
const btnClose = document.createElement('button'); const btnClose = document.createElement('button');
btnClose.className = 'widget-btn widget-close'; btnClose.className = 'widget-btn widget-close';
btnClose.title = 'Schließen'; btnClose.title = t('widget.close');
btnClose.textContent = '\u2715'; btnClose.textContent = '\u2715';
btnClose.addEventListener('click', () => this.close(state.id)); btnClose.addEventListener('click', () => this.close(state.id));