2 Commits

Author SHA1 Message Date
JonKazama-Hellion b55bb7ac34 feat(timer): Timer/Countdown-Widget mit Presets und Alarm
Countdown-Timer als Single-Instance-Widget mit Preset-System
(max. 5), Web Audio API Alarm und Tab-Titel-Blink bei Ablauf.
Mute-Toggle zum Stummschalten des Alarms.
Z-Index-Hierarchie für Widgets auf 100 angehoben.
2026-03-22 00:32:41 +01:00
JonKazama-Hellion 37e45a2041 feat(calculator): Taschenrechner-Widget mit History und Tastatureingabe
Neues Widget-Modul mit Shunting-Yard Parser, 4x5 Button-Grid,
persistenter History (max 10) und Keyboard-Support.
Storage-Handling in Notes/Data erweitert fuer parallele Persistierung.
2026-03-22 00:13:40 +01:00
38 changed files with 215 additions and 2188 deletions
-4
View File
@@ -1,4 +0,0 @@
# Hellion NewTab — Code Owners
# Alle Änderungen müssen von @JonKazama-Hellion approved werden
* @JonKazama-Hellion
-4
View File
@@ -1,4 +0,0 @@
# These are supported funding model platforms
ko_fi: hellionmedia
-4
View File
@@ -16,10 +16,6 @@ dist/
node_modules/
/xpi/
v2-planning.md
themes-v2.md
# Firefox Update-Manifest (wird auf hellion-media.de gehostet)
updates.json
# Persönliche Backup-Dateien (nicht ins Repo)
favorites_*.html
+2 -2
View File
@@ -1,6 +1,6 @@
# ⬡ Hellion Dashboard v1.9.0
# ⬡ Hellion Dashboard v1.5.2
![Version](https://img.shields.io/badge/Version-1.9.0-blue)
![Version](https://img.shields.io/badge/Version-1.5.2-blue)
![JavaScript](https://img.shields.io/badge/JavaScript-Vanilla%20ES2020-F7DF1E?logo=javascript&logoColor=black)
![Manifest](https://img.shields.io/badge/Manifest-V3-green)
![License](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-orange)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

-163
View File
@@ -1,163 +0,0 @@
# Hellion Dashboard — Architecture
## Overview
Hellion Dashboard is a browser extension (NewTab replacement) built with **Vanilla JavaScript ES2020**, **CSS Custom Properties**, and **zero dependencies**. No build step, no framework, no bundler — files are loaded directly via `<script>` tags.
**Storage:** `chrome.storage.local` with `localStorage` fallback.
**Manifest:** V3 for Chromium browsers, V3 for Firefox (separate manifest).
---
## File Structure
```
HOM_NewTab_Project/
├── newtab.html # Single HTML entry point
├── manifest.json # Chrome/Edge/Brave/Vivaldi (MV3)
├── manifest.firefox.json # Firefox (MV3)
├── manifest.opera.json # Opera/Opera GX (MV3 + workarounds)
├── src/
│ ├── css/
│ │ └── main.css # All styles, themes, responsive breakpoints
│ └── js/
│ ├── storage.js # Storage abstraction layer
│ ├── state.js # Global state, defaults, helpers
│ ├── themes.js # Theme definitions & application
│ ├── boards.js # Board/bookmark rendering & events
│ ├── drag.js # Drag & drop (Pointer Events API)
│ ├── settings.js # Settings panel, toggles, theme picker
│ ├── search.js # Search bar (Google, DuckDuckGo, Bing)
│ ├── widgets.js # Widget manager (registry, drag, resize)
│ ├── notes.js # Notes/checklists (multi-instance widgets)
│ ├── calculator.js # Calculator widget (single-instance)
│ ├── timer.js # Timer/countdown widget (single-instance)
│ ├── image-ref.js # Image reference widget (multi-instance)
│ ├── onboarding.js # First-run onboarding flow
│ ├── data.js # JSON export/import (backup & restore)
│ ├── app.js # Init, clock, global events (entry point)
│ └── dialog.js # Custom dialog system (alert, confirm)
├── assets/
│ ├── icons/ # Extension icons (16-512px)
│ └── themes/ # Theme background images
└── docs/ # Documentation (you are here)
```
---
## Module Responsibilities
Each module has exactly one responsibility. They communicate through global references (no import/export — this is a browser extension without a bundler).
| Module | Responsibility |
|---|---|
| `storage.js` | **Only** place that touches `chrome.storage` / `localStorage`. All other modules go through `Store.get()` / `Store.set()`. |
| `state.js` | Global `boards` and `settings` arrays, default values, `uid()`, `escHtml()`, `getFaviconUrl()`. |
| `themes.js` | Theme CSS variable application. 8 themes, each with its own `[data-theme]` block in CSS. |
| `boards.js` | Renders boards and bookmarks. Event delegation on board containers. |
| `drag.js` | Board and bookmark reordering via Pointer Events API. |
| `settings.js` | Settings panel UI, toggle handlers, theme modal, background upload. |
| `search.js` | Search bar with engine switching (Google, DuckDuckGo, Bing). |
| `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. |
| `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. |
| `image-ref.js` | Image reference widget. Multi-instance (max 3). Canvas API WebP conversion, sessionStorage for image data. |
| `onboarding.js` | Multi-slide onboarding flow. Gaming starter board opt-in. |
| `data.js` | JSON export/import with validation. Handles boards, notes, calculator history, timer presets. |
| `app.js` | Entry point. Calls `init()` on DOMContentLoaded. Clock, global event binding. |
| `dialog.js` | `HellionDialog.alert()` and `HellionDialog.confirm()` — custom styled dialogs replacing native browser dialogs. |
---
## Init Sequence
```
DOMContentLoaded
→ init()
→ Store.get('boards') # Load saved boards
→ Store.get('settings') # Load saved settings
→ applySettings() # Apply theme, toggles, etc.
→ renderBoards() # Render all boards
→ startClock() # Start clock/date display
→ bindGlobalEvents() # Header buttons, modals
→ bindSettingsEvents() # Settings toggles, theme picker
→ initSearch() # Search bar
→ migrateSticky() # Legacy sticky note migration
→ Notes.init() # Notes + widget toolbar
→ Calculator.init() # Calculator widget
→ Timer.init() # Timer widget
→ ImageRef.init() # Image reference widget
→ initDataButtons() # Export/import buttons
→ Onboarding check # First-run onboarding
```
---
## Script Load Order
Scripts are loaded in `newtab.html` in dependency order:
```html
<script src="src/js/dialog.js"></script>
<script src="src/js/storage.js"></script>
<script src="src/js/state.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/settings.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/notes.js"></script>
<script src="src/js/calculator.js"></script>
<script src="src/js/timer.js"></script>
<script src="src/js/image-ref.js"></script>
<script src="src/js/data.js"></script>
<script src="src/js/app.js"></script>
```
**Rule:** A module may only reference modules loaded before it.
---
## Z-Index Hierarchy
| Layer | z-index | Elements |
|---|---|---|
| Background | 0-2 | `#bgLayer`, boards |
| Search bar | 90 | `.search-bar-wrapper` |
| Widgets + Toolbar | 100+ | `.widget`, `.widget-toolbar` |
| Header | 100 | `#header` |
| Settings panel | 200 | `#settingsPanel` |
| Dialogs / Modals | 300 | `.hellion-dialog-overlay`, modals |
| Onboarding | 400 | `#onboardingOverlay` |
Widgets use incrementing z-index (`WidgetManager._topZ++`) to stack above each other on click.
---
## Storage Keys
| Key | Type | Content |
|---|---|---|
| `boards` | Array | Board objects with bookmarks |
| `settings` | Object | User preferences (theme, toggles, etc.) |
| `widgetStates` | Object | All widget data (see [widget-schema.md](widget-schema.md)) |
| `onboardingDone` | Boolean | Whether onboarding has been completed |
| `lastBackupReminder` | Number | Timestamp of last backup reminder |
---
## Browser Compatibility
| Browser | Engine | Manifest |
|---|---|---|
| Chrome | Chromium MV3 | `manifest.json` |
| Edge | Chromium MV3 | `manifest.json` |
| Brave | Chromium MV3 | `manifest.json` |
| Vivaldi | Chromium MV3 | `manifest.json` |
| Opera / GX | Chromium MV3 | `manifest.opera.json` |
| Firefox | Gecko MV3 | `manifest.firefox.json` |
Changes affecting manifest fields must be synchronized across all three manifest files.
-310
View File
@@ -1,310 +0,0 @@
# Hellion Dashboard — Code Patterns & Conventions
## Core Principles
- **Vanilla JS ES2020** — No frameworks, no TypeScript, no build step
- **Zero dependencies** — Everything is built from scratch
- **`createElement` only** — Never use `innerHTML` (XSS prevention)
- **CSS Custom Properties** — No hardcoded colors, everything through `var(--name)`
- **Event delegation** — One listener per container, not per element
- **Storage abstraction** — All storage access through `Store.get()` / `Store.set()`
---
## Pattern: Storage Abstraction
**File:** `src/js/storage.js`
All persistent data goes through the `Store` object. Never access `chrome.storage` or `localStorage` directly.
```javascript
// Reading
const boards = await Store.get('boards'); // Returns null if not found
const settings = await Store.get('settings');
// Writing
await Store.set('boards', boards);
await Store.set('settings', settings);
// Quota check (chrome.storage only, 10 MB limit)
await Store.checkQuota();
```
**Why?** The `Store` handles the chrome.storage / localStorage fallback transparently. It also provides unified error handling (shows a dialog when storage is full).
---
## Pattern: Event Delegation
Instead of attaching listeners to each element, attach one to the container and use `closest()` to find the target.
```javascript
// GOOD — one listener, handles all bookmarks
container.addEventListener('click', (e) => {
const bmItem = e.target.closest('.bm-item');
if (!bmItem) return;
const id = bmItem.dataset.id;
// Handle click
});
// BAD — listener per element
bookmarks.forEach(bm => {
bm.addEventListener('click', handler); // Don't do this!
});
```
**Used in:** `boards.js` (board/bookmark events), `notes.js` (toolbar), `calculator.js` (button grid)
---
## Pattern: createElement over innerHTML
Always build DOM with `document.createElement()`. This prevents XSS and is the project's #1 security rule.
```javascript
// GOOD
const link = document.createElement('a');
link.href = bookmark.url;
link.textContent = bookmark.title;
container.appendChild(link);
// BAD — XSS risk!
container.innerHTML = `<a href="${url}">${title}</a>`;
```
---
## Pattern: Shared Storage Key
Multiple widget modules share the `widgetStates` key. Every module must read-before-write and preserve other modules' data.
```javascript
async save() {
const data = await Store.get('widgetStates') || {};
// Write your own data
data.yourKey = { /* ... */ };
// DON'T overwrite — the key already contains other modules' data
await Store.set('widgetStates', data);
}
```
See [widget-schema.md](widget-schema.md) for the full `widgetStates` structure.
---
## Pattern: Widget Lifecycle Hooks
Single-instance widgets (Calculator, Timer) need to know when they're closed, minimized, or reopened. They wrap `WidgetManager` methods in their `init()`:
```javascript
async init() {
// Wrap close
const prevClose = WidgetManager.close;
const self = this;
WidgetManager.close = function(id) {
prevClose.call(WidgetManager, id);
if (id === self.WIDGET_ID) {
self.onClose();
}
};
// Wrap minimize
const prevMinimize = WidgetManager.minimize;
WidgetManager.minimize = async function(id) {
await prevMinimize.call(WidgetManager, id);
if (id === self.WIDGET_ID) {
self._isOpen = false;
await self.save();
}
};
}
```
**Important:** Multiple widgets chain these wraps. Calculator wraps first, Timer wraps Calculator's already-wrapped version, and so on. The chain must not break.
---
## Pattern: Debounced Save
For frequent updates (typing in notes, moving widgets), use debounced saves to avoid excessive storage writes:
```javascript
_saveTimer: null,
_debouncedSave() {
clearTimeout(this._saveTimer);
this._saveTimer = setTimeout(() => this.save(), 500);
}
// Usage: call _debouncedSave() instead of save() for frequent events
textarea.addEventListener('input', () => {
noteData.content = textarea.value;
this._debouncedSave();
});
```
**Used in:** `notes.js` (text editing), `image-ref.js` (label editing)
---
## Pattern: Theme System
All themes use CSS Custom Properties defined in `[data-theme="name"]` blocks:
```css
[data-theme="nebula"] {
--bg-primary: #0a0e17;
--bg-board: rgba(15, 20, 35, 0.65);
--text-primary: #e0e6f0;
--accent: #7db3ff;
--border: rgba(125, 179, 255, 0.12);
/* ... more variables */
}
```
**Never hardcode colors in JS.** Use CSS classes or variables:
```javascript
// GOOD — let CSS handle colors
element.classList.add('active');
// BAD — hardcoded color
element.style.color = '#7db3ff';
```
8 themes are available: Nebula, Crescent, Event Horizon, Merchantman, Julia & Jin, SC Sunset, Hellion HUD, Hellion Energy.
---
## Pattern: Onboarding Slides
The onboarding system (`onboarding.js`) uses a data-driven slide array. Each slide is an object with rendering hints:
```javascript
{
hero: '🎮', // Large emoji/icon
title: 'Slide Title', // Heading
text: 'Description...', // Optional text paragraph
features: ['Item 1', ...], // Optional bullet list
showThemes: true, // Optional theme grid
interactive: 'gaming-board' // Optional custom buttons
}
```
The `_render()` method reads these properties and builds the DOM. To add a new slide, just add an object to the `slides` array.
---
## Pattern: Dialog System
Custom dialogs replace native `alert()` and `confirm()`:
```javascript
// Alert (informational)
await HellionDialog.alert('Message text', {
type: 'info', // 'info', 'success', 'warning', 'danger'
title: 'Title'
});
// Confirm (yes/no)
const ok = await HellionDialog.confirm('Are you sure?', {
type: 'danger',
title: 'Delete',
confirmText: 'Delete', // Custom button text
cancelText: 'Cancel'
});
if (ok) { /* user confirmed */ }
```
---
## Pattern: Pointer Events for Drag
Widget dragging and board reordering use the Pointer Events API (not mouse events):
```javascript
element.addEventListener('pointerdown', (e) => {
element.setPointerCapture(e.pointerId);
function onMove(ev) {
// Update position
}
function onUp() {
element.releasePointerCapture(e.pointerId);
element.removeEventListener('pointermove', onMove);
element.removeEventListener('pointerup', onUp);
}
element.addEventListener('pointermove', onMove);
element.addEventListener('pointerup', onUp);
});
```
**Why Pointer Events over Mouse Events?** They work with both mouse and touch, and `setPointerCapture` ensures events continue even if the cursor leaves the element.
---
## Pattern: Canvas API Image Processing
The image reference widget converts uploaded images to WebP for smaller size:
```javascript
_processFile(file) {
return new Promise((resolve, reject) => {
const objectUrl = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const webpUrl = canvas.toDataURL('image/webp', 0.85);
URL.revokeObjectURL(objectUrl);
resolve(webpUrl);
};
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error('Image could not be loaded'));
};
img.src = objectUrl;
});
}
```
**Important:** Always call `URL.revokeObjectURL()` to free memory.
---
## Coding Rules Summary
| Rule | Rationale |
|---|---|
| `createElement` only, never `innerHTML` | XSS prevention |
| All storage through `Store` | Browser compatibility |
| CSS variables, no hardcoded colors | Theme support |
| Event delegation | Performance, dynamic content |
| `const`/`let`, never `var` | Block scoping |
| No external dependencies | Extension simplicity |
| No build step | Direct development |
| JSDoc comments on public functions | Documentation |
| URL validation before `href` | Security |
| Error handling on storage operations | Graceful failure |
---
## Manifest Synchronization
Three manifest files must stay in sync:
- `manifest.json` — Chrome, Edge, Brave, Vivaldi
- `manifest.firefox.json` — Firefox
- `manifest.opera.json` — Opera, Opera GX
When changing version numbers, permissions, or content script entries, update all three files.
-330
View File
@@ -1,330 +0,0 @@
# Hellion Dashboard — Widget Schema
## Overview
The widget system provides draggable, resizable floating panels managed by `WidgetManager` (`src/js/widgets.js`). Each widget type has its own module that handles content rendering and state management.
---
## Widget Types
| Type | Module | Instance | Max | Storage |
|---|---|---|---|---|
| `note` | `notes.js` | Multi | 5 | Persistent (`widgetStates.notes`) |
| `calculator` | `calculator.js` | Single | 1 | Persistent (`widgetStates.calculator`) |
| `timer` | `timer.js` | Single | 1 | Persistent (`widgetStates.timer`) |
| `image` | `image-ref.js` | Multi | 3 | Meta: persistent, Image data: sessionStorage |
---
## WidgetManager API
### `create(type, config) → string`
Creates a widget and appends it to the DOM.
```javascript
const id = WidgetManager.create('note', {
id: 'note_abc123', // Optional, auto-generated if omitted
title: 'My Note', // Default: 'Note'
x: 120, // Left position in px
y: 80, // Top position in px
width: 280, // Width in px (min: 200)
height: 220, // Height in px (min: 150)
open: true // Visible state (default: true)
});
```
### `getBody(id) → HTMLElement | null`
Returns the `.widget-body` element for content rendering.
```javascript
const body = WidgetManager.getBody('widget_calculator');
if (body) Calculator.renderBody(body);
```
### `getState(id) → Object | null`
Returns the current widget state (position, size, open status).
```javascript
const state = WidgetManager.getState('widget_timer');
// → { id, type, title, x, y, width, height, open }
```
### `close(id)`
Permanently removes a widget from the DOM and registry.
### `minimize(id)`
Hides a widget with animation. Widget remains in registry with `open: false`.
### `openWidget(id)`
Restores a minimized widget with animation.
### `bringToFront(id)`
Increments z-index to bring widget above all others.
### `save() → Array`
Returns an array of all `type: 'note'` widget states. Used by `Notes.save()` to merge with note content data.
### `restore(renderCallback)`
Loads widget states from storage and recreates all note widgets. Only handles notes — single-instance widgets (calculator, timer) restore themselves in their own `init()`.
---
## Shared Storage Key: `widgetStates`
All widget modules share a single storage key. Each module's `save()` method must preserve other modules' data.
```javascript
// Structure of widgetStates
{
notes: [
{
id: 'note_abc123',
title: 'My Note',
content: 'Hello world',
template: 'text', // 'text' or 'checklist'
x: 120, y: 80,
width: 280, height: 220,
open: true,
checklistItems: [], // For checklist template
checkedItems: [] // Checked item IDs
}
],
calculator: {
x: 400, y: 120,
width: 280, height: 400,
open: false,
history: [
{ expr: '2 + 3', result: '5' }
]
},
timer: {
x: 600, y: 80,
width: 260, height: 360,
open: false,
muted: false,
presets: [
{ name: 'Forschung', seconds: 2700 }
]
},
imageRef: {
images: [
{
id: 'image_0',
label: 'Bauplan',
x: 200, y: 120,
width: 320, height: 280,
open: true
}
]
}
}
```
### Save Pattern — Preserving Other Modules' Data
Every module that saves to `widgetStates` must read existing data first and preserve keys it doesn't own:
```javascript
// Example from notes.js
async save() {
const existing = await Store.get(this.STORAGE_KEY);
const saveData = { notes: mergedNotes };
// Preserve other modules
if (existing && existing.calculator) saveData.calculator = existing.calculator;
if (existing && existing.timer) saveData.timer = existing.timer;
if (existing && existing.imageRef) saveData.imageRef = existing.imageRef;
await Store.set(this.STORAGE_KEY, saveData);
}
```
---
## Creating a New Widget Type
### Step 1: Choose Single or Multi-Instance
- **Single-instance** (like Calculator, Timer): One widget with a fixed ID. `toggle()` opens/closes.
- **Multi-instance** (like Notes, ImageRef): Multiple widgets with dynamic IDs. `create()` adds new ones.
### Step 2: Create the Module (`src/js/your-widget.js`)
```javascript
const YourWidget = {
WIDGET_ID: 'widget_yourwidget', // Fixed ID for single-instance
STORAGE_KEY: 'widgetStates',
_isOpen: false,
// Load state from storage
async load() {
const data = await Store.get(this.STORAGE_KEY);
if (data && data.yourWidget) {
// Restore your state
}
},
// Save state, preserving other modules
async save() {
const data = await Store.get(this.STORAGE_KEY) || {};
if (data.notes === undefined) data.notes = [];
const widgetState = WidgetManager.getState(this.WIDGET_ID);
data.yourWidget = {
x: widgetState ? widgetState.x : 400,
y: widgetState ? widgetState.y : 120,
width: widgetState ? widgetState.width : 280,
height: widgetState ? widgetState.height : 300,
open: this._isOpen,
// ... your custom data
};
await Store.set(this.STORAGE_KEY, data);
},
// Open widget
async open() {
if (this._isOpen) {
WidgetManager.bringToFront(this.WIDGET_ID);
return;
}
const data = await Store.get(this.STORAGE_KEY);
const saved = (data && data.yourWidget) ? data.yourWidget : {};
WidgetManager.create('yourwidget', {
id: this.WIDGET_ID,
title: 'Your Widget',
x: saved.x || 400,
y: saved.y || 120,
width: saved.width || 280,
height: saved.height || 300,
open: true
});
const body = WidgetManager.getBody(this.WIDGET_ID);
if (body) this.renderBody(body);
this._isOpen = true;
await this.save();
},
// Toggle open/close
async toggle() {
if (this._isOpen) {
const entry = WidgetManager._widgets.get(this.WIDGET_ID);
if (entry && entry.state.open) {
await WidgetManager.minimize(this.WIDGET_ID);
this._isOpen = false;
await this.save();
} else if (entry) {
await WidgetManager.openWidget(this.WIDGET_ID);
this._isOpen = true;
await this.save();
}
} else {
await this.open();
}
},
// Render widget content
renderBody(bodyEl) {
bodyEl.textContent = '';
// Build your UI with createElement (never innerHTML!)
},
// Initialize and hook into lifecycle
async init() {
await this.load();
// Restore if was open last time
const data = await Store.get(this.STORAGE_KEY);
if (data && data.yourWidget && data.yourWidget.open) {
await this.open();
}
// Hook into close event
const self = this;
const prevClose = WidgetManager.close;
WidgetManager.close = function(id) {
prevClose.call(WidgetManager, id);
if (id === self.WIDGET_ID) {
self._isOpen = false;
self.save();
}
};
// Hook into minimize event
const prevMinimize = WidgetManager.minimize;
WidgetManager.minimize = async function(id) {
await prevMinimize.call(WidgetManager, id);
if (id === self.WIDGET_ID) {
self._isOpen = false;
await self.save();
}
};
// Hook into open event
const prevOpen = WidgetManager.openWidget;
WidgetManager.openWidget = async function(id) {
await prevOpen.call(WidgetManager, id);
if (id === self.WIDGET_ID) {
self._isOpen = true;
const body = WidgetManager.getBody(self.WIDGET_ID);
if (body && body.children.length === 0) {
self.renderBody(body);
}
await self.save();
}
};
}
};
```
### Step 3: Integration Checklist
1. **`newtab.html`** — Add `<script>` tag (after `widgets.js`, before `data.js`)
2. **`newtab.html`** — Add toolbar button: `<button class="widget-toolbar-btn" data-action="your-action">`
3. **`notes.js`** — Add toolbar handler in `initToolbar()`: `} else if (action === 'your-action') { YourWidget.toggle(); }`
4. **`notes.js`** — Preserve your data in `save()`: `if (existing && existing.yourWidget) saveData.yourWidget = existing.yourWidget;`
5. **`app.js`** — Add `await YourWidget.init();` to the init sequence
6. **`src/css/main.css`** — Add widget-specific CSS styles
7. **`data.js`** — Add export/import logic (if data should be included in backups)
---
## Widget DOM Structure
Every widget created by `WidgetManager.create()` has this structure:
```html
<div class="widget" data-widget-id="widget_abc123"
style="left: 120px; top: 80px; width: 280px; height: 220px;">
<div class="widget-header">
<span class="widget-title">Title</span>
<div class="widget-actions">
<button class="widget-btn widget-minimize"></button>
<button class="widget-btn widget-close"></button>
</div>
</div>
<div class="widget-body">
<!-- Your content goes here (via renderBody) -->
</div>
<div class="widget-resize-handle"></div>
</div>
```
- **Header** is the drag handle (Pointer Events)
- **Title** supports double-click to edit (contentEditable, max 20 chars)
- **Body** is where your module renders content
- **Resize handle** appears on hover (bottom-right corner)
+1 -2
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hellion NewTab",
"version": "1.11.1",
"version": "1.5.2",
"description": "Personal bookmark dashboard — local, private, no account needed. By Hellion Online Media.",
"author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de",
@@ -18,7 +18,6 @@
"browser_specific_settings": {
"gecko": {
"id": "hellion-newtab@hellion-media.de",
"update_url": "https://hellion-media.de/extensions/firefox/updates.json",
"strict_min_version": "142.0",
"data_collection_permissions": {
"required": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hellion NewTab",
"version": "1.11.1",
"version": "1.5.2",
"description": "Personal bookmark dashboard — local, private, no account needed. By Hellion Online Media.",
"author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hellion Dashboard (GX Native)",
"version": "1.11.1",
"version": "1.5.2",
"description": "Ersetzt die Opera GX Startseite durch dein persönliches, leistungsoptimiertes Hellion Dashboard. Schnell, sauber und werbefrei.",
"author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de",
+132 -136
View File
@@ -35,9 +35,9 @@
<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
</button>
<button class="btn-icon" id="btnTheme" title="Darstellung & Theme">
<button class="btn-icon" id="btnTheme" title="Theme wählen">
<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
Theme
</button>
<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>
@@ -73,9 +73,6 @@
<button class="widget-toolbar-btn" data-action="timer" title="Timer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/><path d="M5 3l2 2"/><path d="M19 3l-2 2"/><line x1="12" y1="1" x2="12" y2="3"/></svg>
</button>
<button class="widget-toolbar-btn hidden" data-action="image-ref" 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>
</button>
<button class="widget-toolbar-btn" data-action="notebook" title="Alle Notes">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
</button>
@@ -105,11 +102,84 @@
<div class="panel-overlay" id="settingsOverlay"></div>
<aside class="settings-panel" id="settingsPanel">
<div class="panel-header">
<span>Einstellungen</span>
<span>Settings</span>
<button class="btn-close" id="btnCloseSettings"></button>
</div>
<div class="panel-body">
<!-- APPEARANCE -->
<section class="settings-section" data-section="appearance">
<button class="settings-section-title" type="button">
<span class="section-chevron"></span>
APPEARANCE
</button>
<div class="section-content">
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Compact mode</span>
<span class="setting-desc">Reduce spacing to show more bookmarks</span>
</div>
<label class="toggle"><input type="checkbox" id="settingCompact" /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Shorten long titles</span>
<span class="setting-desc">Shorten title to one line with "…"</span>
</div>
<label class="toggle"><input type="checkbox" id="settingShorten" /><span class="slider"></span></label>
</div>
</div>
</section>
<!-- BEHAVIOR -->
<section class="settings-section" data-section="behavior">
<button class="settings-section-title" type="button">
<span class="section-chevron"></span>
BEHAVIOR
</button>
<div class="section-content">
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Open links in new tab</span>
<span class="setting-desc">Open bookmarks in a new browser tab</span>
</div>
<label class="toggle"><input type="checkbox" id="settingNewTab" checked /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Show bookmark descriptions</span>
<span class="setting-desc">Display saved descriptions below bookmark titles</span>
</div>
<label class="toggle"><input type="checkbox" id="settingShowDesc" /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Hide extra bookmarks in long boards</span>
<span class="setting-desc">Automatically hides extra bookmarks in long boards</span>
</div>
<label class="toggle"><input type="checkbox" id="settingHideExtra" /><span class="slider"></span></label>
</div>
<div class="setting-row" id="visibleCountRow">
<div class="setting-info">
<span class="setting-label">Visible bookmarks before hide</span>
<span class="setting-desc">Choose how many bookmarks are shown</span>
</div>
<select class="select-input" id="settingVisibleCount">
<option value="5">5</option>
<option value="10" selected>10</option>
<option value="20">20</option>
</select>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Suchleiste anzeigen</span>
<span class="setting-desc">Suchleiste unter dem Header ein/aus</span>
</div>
<label class="toggle"><input type="checkbox" id="settingShowSearch" checked /><span class="slider"></span></label>
</div>
</div>
</section>
<!-- WIDGETS -->
<section class="settings-section" data-section="widgets">
<button class="settings-section-title" type="button">
@@ -120,55 +190,48 @@
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Toolbar-Position</span>
<span class="setting-desc">Widget-Toolbar links oder rechts anzeigen</span>
<span class="setting-desc">Widget-Toolbar links oder rechts</span>
</div>
<select class="select-input" id="settingToolbarPos">
<option value="right" selected>Rechts</option>
<option value="left">Links</option>
</select>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Bild-Referenz Widgets</span>
<span class="setting-desc">Bilder als Referenz anzeigen (nur aktuelle Session)</span>
</div>
<label class="toggle">
<input type="checkbox" id="settingImageRef">
<span class="slider"></span>
</label>
</div>
</div>
</section>
<!-- DATEN & HILFE -->
<!-- DATA -->
<section class="settings-section" data-section="data">
<button class="settings-section-title" type="button">
<span class="section-chevron"></span>
DATEN & HILFE
DATA
</button>
<div class="section-content">
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Backup exportieren</span>
<span class="setting-desc">Alle Boards, Notes und Einstellungen als JSON sichern</span>
<span class="setting-label">Export Boards</span>
<span class="setting-desc">Alle Boards als JSON sichern</span>
</div>
<button class="btn-small" id="btnExportJSON">Export</button>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Backup importieren</span>
<span class="setting-label">Import Boards</span>
<span class="setting-desc">JSON-Backup wiederherstellen</span>
</div>
<button class="btn-small" id="btnImportJSON">Import</button>
<input type="file" id="jsonImportInput" accept=".json" class="hidden" />
</div>
<div class="setting-row" id="browserImportRow">
<div class="setting-info">
<span class="setting-label">Browser-Lesezeichen</span>
<span class="setting-desc">Lesezeichen direkt aus dem Browser importieren</span>
</div>
<button class="btn-small" id="btnBrowserImport">Import</button>
</div>
</section>
<!-- HELP -->
<section class="settings-section" data-section="help">
<button class="settings-section-title" type="button">
<span class="section-chevron"></span>
HELP
</button>
<div class="section-content">
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Onboarding wiederholen</span>
@@ -179,30 +242,16 @@
</div>
</section>
<!-- DANGER ZONE -->
<section class="settings-section" data-section="danger">
<button class="settings-section-title danger" type="button">
<!-- ABOUT / IMPRESSUM -->
<section class="settings-section" data-section="about">
<button class="settings-section-title" type="button">
<span class="section-chevron"></span>
DANGER ZONE
ABOUT
</button>
<div class="section-content">
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Alles zurücksetzen</span>
<span class="setting-desc">Löscht alle Boards, Notes und Einstellungen</span>
</div>
<button class="btn-danger" id="btnResetAll">Reset</button>
</div>
</div>
</section>
</div>
<!-- ABOUT — fixiert am unteren Rand -->
<div class="panel-footer">
<div class="about-block">
<div class="about-logo">⬡ HELLION NEWTAB</div>
<div class="about-version">Version 1.11.1 · by Hellion Online Media</div>
<div class="about-version">Version 1.5.2 · by Hellion Online Media</div>
<div class="about-links">
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
@@ -266,28 +315,48 @@
</div>
</div>
</div>
</section>
<!-- DANGER ZONE -->
<section class="settings-section" data-section="danger">
<button class="settings-section-title danger" type="button">
<span class="section-chevron"></span>
DANGER ZONE
</button>
<div class="section-content">
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Reset all data</span>
<span class="setting-desc">Deletes all boards and bookmarks</span>
</div>
<button class="btn-danger" id="btnResetAll">Reset</button>
</div>
</div>
</section>
</div>
</aside>
<!-- THEME PICKER MODAL -->
<div class="modal-overlay" id="themeOverlay">
<div class="theme-modal" id="themeModal">
<div class="modal-header">
<span>Darstellung</span>
<span>Theme wählen</span>
<button class="btn-close" id="btnCloseTheme"></button>
</div>
<div class="theme-grid">
<div class="theme-card active" data-value="nebula">
<img class="theme-card-img" src="assets/themes/bg-nebula.webp" alt="Nebula" />
<img class="theme-card-img" src="assets/themes/bg-nebula.jpg" alt="Nebula" />
<span class="theme-card-label">Nebula</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="crescent">
<img class="theme-card-img" src="assets/themes/bg-crescent.webp" alt="Crescent" />
<img class="theme-card-img" src="assets/themes/bg-crescent.jpg" alt="Crescent" />
<span class="theme-card-label">Crescent</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="event-horizon">
<img class="theme-card-img" src="assets/themes/bg-event-horizon.webp" alt="Event Horizon" />
<img class="theme-card-img" src="assets/themes/bg-event-horizon.jpg" alt="Event Horizon" />
<span class="theme-card-label">Event Horizon</span>
<span class="theme-card-check"></span>
</div>
@@ -297,119 +366,48 @@
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="julia-jin">
<img class="theme-card-img" src="assets/themes/bg-julia-jin.webp" alt="Julia &amp; Jin" />
<img class="theme-card-img" src="assets/themes/bg-julia-jin.png" alt="Julia &amp; Jin" />
<span class="theme-card-label">Julia &amp; Jin</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="sc-sunset">
<img class="theme-card-img" src="assets/themes/bg-sc-sunset.webp" alt="SC Sunset" />
<img class="theme-card-img" src="assets/themes/bg-sc-sunset.jpg" alt="SC Sunset" />
<span class="theme-card-label">SC Sunset</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="hellion-hud">
<img class="theme-card-img" src="assets/themes/bg-hellion-hud.webp" alt="Hellion HUD" />
<img class="theme-card-img" src="assets/themes/bg-hellion-hud.png" alt="Hellion HUD" />
<span class="theme-card-label">HUD</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="hellion-energy">
<img class="theme-card-img" src="assets/themes/bg-hellion-energy.webp" alt="Hellion Energy" />
<img class="theme-card-img" src="assets/themes/bg-hellion-energy.jpg" alt="Hellion Energy" />
<span class="theme-card-label">Energy</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="satisfactory">
<img class="theme-card-img" src="assets/themes/bg-satisfactory.webp" alt="Satisfactory" />
<span class="theme-card-label">Satisfactory</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="avorion">
<img class="theme-card-img" src="assets/themes/bg-avorion.webp" alt="Avorion" />
<span class="theme-card-label">Avorion</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card" data-value="hellion-stealth">
<img class="theme-card-img" src="assets/themes/bg-scPolaris.webp" alt="Hellion Stealth" />
<span class="theme-card-label">Stealth</span>
<span class="theme-card-check"></span>
</div>
</div>
<div class="theme-modal-section">
<h3 class="settings-section-title">HINTERGRUND</h3>
<h3 class="settings-section-title">BACKGROUND</h3>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Bild-URL</span>
<span class="setting-desc">Eigenes Hintergrundbild per URL</span>
<span class="setting-label">Background image URL</span>
<span class="setting-desc">Custom wallpaper URL</span>
</div>
<button class="btn-small" id="btnChangeBg">Ändern</button>
<button class="btn-small" id="btnChangeBg">Change</button>
</div>
<div class="setting-row hidden" id="bgInputRow">
<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>
<input type="text" class="text-input full-width" id="bgUrlInput" placeholder="https://... or leave empty for default" />
<button class="btn-small" id="btnApplyBg">Apply</button>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Datei hochladen</span>
<span class="setting-desc">Lokales Bild als Hintergrund verwenden</span>
<span class="setting-label">Background file upload</span>
<span class="setting-desc">Use a local image as background</span>
</div>
<button class="btn-small" id="btnBgFile">Upload</button>
<input type="file" id="bgFileInput" accept="image/*" class="hidden" />
</div>
</div>
<div class="theme-modal-section">
<h3 class="settings-section-title">DARSTELLUNG</h3>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Kompaktmodus</span>
<span class="setting-desc">Weniger Abstand für mehr Bookmarks</span>
</div>
<label class="toggle"><input type="checkbox" id="settingCompact" /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Lange Titel kürzen</span>
<span class="setting-desc">Titel auf eine Zeile mit „…" kürzen</span>
</div>
<label class="toggle"><input type="checkbox" id="settingShorten" /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Suchleiste anzeigen</span>
<span class="setting-desc">Suchleiste unter dem Header ein/aus</span>
</div>
<label class="toggle"><input type="checkbox" id="settingShowSearch" checked /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Links in neuem Tab</span>
<span class="setting-desc">Bookmarks in neuem Browser-Tab öffnen</span>
</div>
<label class="toggle"><input type="checkbox" id="settingNewTab" checked /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Beschreibungen anzeigen</span>
<span class="setting-desc">Gespeicherte Beschreibung unter Bookmarks</span>
</div>
<label class="toggle"><input type="checkbox" id="settingShowDesc" /><span class="slider"></span></label>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">Bookmarks ausblenden</span>
<span class="setting-desc">Überzählige Bookmarks in langen Boards verstecken</span>
</div>
<label class="toggle"><input type="checkbox" id="settingHideExtra" /><span class="slider"></span></label>
</div>
<div class="setting-row" id="visibleCountRow">
<div class="setting-info">
<span class="setting-label">Sichtbare Bookmarks</span>
<span class="setting-desc">Anzahl vor dem Ausblenden</span>
</div>
<select class="select-input" id="settingVisibleCount">
<option value="5">5</option>
<option value="10" selected>10</option>
<option value="20">20</option>
</select>
</div>
</div>
</div>
</div>
@@ -485,8 +483,6 @@
<script src="src/js/notes.js"></script>
<script src="src/js/calculator.js"></script>
<script src="src/js/timer.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>
<!-- Onboarding -->
<script src="src/js/onboarding.js"></script>
+2 -272
View File
@@ -298,97 +298,6 @@
box-shadow: inset 0 0 10px rgba(30, 255, 142, 0.05);
}
/* ============================================
THEME: SATISFACTORY (Industrial Desert)
============================================ */
[data-theme="satisfactory"] {
--accent: #00b4d8;
--accent-dim: rgba(0, 180, 216, 0.12);
--accent-glow: rgba(0, 180, 216, 0.08);
--border-accent: rgba(0, 180, 216, 0.35);
--bg-primary: #1a0f08;
--bg-board: rgba(26, 15, 8, 0.65);
--border: rgba(0, 180, 216, 0.15);
--text-primary: #f0faff;
--text-secondary: #a89f98;
--text-muted: #635a54;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: linear-gradient(180deg,
rgba(26,15,8,0.85) 0%,
rgba(26,15,8,0.15) 50%,
rgba(26,15,8,0.90) 100%);
--header-bg: rgba(26,15,8,0.95);
--board-hover-border: rgba(0, 180, 216, 0.25);
--toggle-on-bg: rgba(0, 180, 216, 0.20);
--logo-shadow: rgba(0, 180, 216, 0.40);
}
[data-theme="satisfactory"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; }
[data-theme="satisfactory"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
[data-theme="satisfactory"] .board-title { font-family: 'Rajdhani', sans-serif; letter-spacing: 1.5px; text-transform: uppercase; }
[data-theme="satisfactory"] .board { border-color: rgba(0, 180, 216, 0.20); backdrop-filter: blur(12px); }
[data-theme="satisfactory"] .bm-item:hover { background: rgba(0, 180, 216, 0.10); }
/* ============================================
THEME: AVORION (Deep Void)
============================================ */
[data-theme="avorion"] {
--accent: #2ec4a0;
--accent-dim: rgba(46, 196, 160, 0.12);
--accent-glow: rgba(46, 196, 160, 0.08);
--border-accent: rgba(46, 196, 160, 0.30);
--bg-primary: #020d0c;
--bg-board: rgba(2, 13, 12, 0.60);
--border: rgba(46, 196, 160, 0.12);
--text-primary: #e6fffa;
--text-secondary: #8abdb3;
--text-muted: #40615a;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: radial-gradient(circle at center,
transparent 0%,
rgba(2, 13, 12, 0.95) 100%);
--header-bg: rgba(2, 13, 12, 0.94);
--board-hover-border: rgba(46, 196, 160, 0.22);
--toggle-on-bg: rgba(46, 196, 160, 0.18);
--logo-shadow: rgba(46, 196, 160, 0.50);
}
[data-theme="avorion"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 500; letter-spacing: 6px; text-transform: uppercase; }
[data-theme="avorion"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 400; color: var(--accent); }
[data-theme="avorion"] .board-title { font-family: 'Rajdhani', sans-serif; font-weight: 500; text-transform: uppercase; }
[data-theme="avorion"] .board { border-color: rgba(46, 196, 160, 0.15); backdrop-filter: blur(8px); }
[data-theme="avorion"] .bm-item:hover { background: rgba(46, 196, 160, 0.08); }
/* ============================================
THEME: HELLION STEALTH (Tactical Recon)
============================================ */
[data-theme="hellion-stealth"] {
--accent: #5ec2ff;
--accent-dim: rgba(94, 194, 255, 0.12);
--accent-glow: rgba(94, 194, 255, 0.08);
--border-accent: rgba(94, 194, 255, 0.35);
--bg-primary: #0d0f12;
--bg-board: rgba(13, 15, 18, 0.70);
--border: rgba(94, 194, 255, 0.15);
--text-primary: #e0f4ff;
--text-secondary: #8a9499;
--text-muted: #4a5257;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: radial-gradient(circle at center,
transparent 0%,
rgba(13, 15, 18, 0.90) 100%);
--header-bg: rgba(13, 15, 18, 0.96);
--board-hover-border: rgba(94, 194, 255, 0.25);
--toggle-on-bg: rgba(94, 194, 255, 0.20);
--logo-shadow: rgba(94, 194, 255, 0.45);
}
[data-theme="hellion-stealth"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 4px; }
[data-theme="hellion-stealth"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); }
[data-theme="hellion-stealth"] .board-title { text-transform: uppercase; font-size: 0.85rem; letter-spacing: 2px; }
[data-theme="hellion-stealth"] .board { border-color: rgba(94, 194, 255, 0.15); backdrop-filter: blur(10px); }
[data-theme="hellion-stealth"] .bm-item:hover { background: rgba(94, 194, 255, 0.10); border-left: 2px solid var(--accent); }
/* ============================================
BASE STYLES
============================================ */
@@ -628,13 +537,7 @@ body.show-desc .bm-desc { display: block; }
font-family: var(--font-display); font-size: 15px; font-weight: 600;
letter-spacing: 2px; color: var(--accent); text-transform: uppercase;
}
.panel-body { flex: 1; overflow-y: auto; padding: 12px 0; scrollbar-width: thin; scrollbar-color: var(--border) transparent; min-height: 0; }
.panel-footer {
flex-shrink: 0; border-top: 1px solid var(--border);
max-height: 45vh; overflow-y: auto;
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.panel-footer .about-block { padding: 12px 18px 16px; }
.panel-body { flex: 1; overflow-y: auto; padding: 12px 0; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
.settings-section { margin-bottom: 4px; }
.settings-section-title {
@@ -703,9 +606,6 @@ body.show-desc .bm-desc { display: block; }
.theme-card[data-value="sc-sunset"] .theme-card-label { color: #ff8c3d; } /* Amber Sunset */
.theme-card[data-value="hellion-hud"] .theme-card-label { color: #32ff6a; } /* Neon Green */
.theme-card[data-value="hellion-energy"] .theme-card-label { color: #1eff8e; } /* Acid Green */
.theme-card[data-value="satisfactory"] .theme-card-label { color: #00b4d8; } /* Cyan LED */
.theme-card[data-value="avorion"] .theme-card-label { color: #2ec4a0; } /* Turquoise */
.theme-card[data-value="hellion-stealth"] .theme-card-label { color: #5ec2ff; } /* Tech Blue */
.theme-card:hover .theme-card-label {
text-shadow: 0 0 8px currentColor;
transition: text-shadow 0.2s ease;
@@ -739,12 +639,11 @@ body.show-desc .bm-desc { display: block; }
/* INPUTS */
.select-input {
padding: 5px 8px; background: var(--bg-board, rgba(255,255,255,0.06));
padding: 5px 8px; background: rgba(255,255,255,0.06);
border: 1px solid var(--border); border-radius: var(--radius-sm);
color: var(--text-primary); font-family: var(--font-body); font-size: 12px;
cursor: pointer; min-width: 70px;
}
.select-input option { background: var(--bg-primary, #0a0e17); color: var(--text-primary); }
.select-input:focus { outline: none; border-color: var(--border-accent); }
.text-input {
@@ -1465,91 +1364,6 @@ body.show-desc .bm-desc { display: block; }
color: var(--bg-primary);
}
/* ============================================
IMAGE REFERENCE WIDGET
============================================ */
.imgref-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 8px;
}
.imgref-img-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: var(--radius-sm);
min-height: 80px;
}
.imgref-img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: var(--radius-sm);
}
.imgref-dropzone {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px dashed var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
font-size: 12px;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
min-height: 80px;
gap: 8px;
}
.imgref-dropzone:hover,
.imgref-dropzone.dragover {
border-color: var(--accent);
color: var(--text-secondary);
}
.imgref-dropzone-icon {
font-size: 24px;
opacity: 0.5;
}
.imgref-label {
width: 100%;
margin-top: 8px;
font-size: 11px;
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-secondary);
padding: 4px 8px;
flex-shrink: 0;
}
.imgref-label:focus {
border-color: var(--accent);
outline: none;
}
.imgref-label::placeholder {
color: var(--text-muted);
}
.imgref-replace-btn {
width: 100%;
height: 24px;
margin-top: 6px;
font-size: 10px;
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
cursor: pointer;
flex-shrink: 0;
transition: all 0.2s;
}
.imgref-replace-btn:hover {
border-color: var(--accent);
color: var(--text-secondary);
}
/* ============================================
WIDGET TOOLBAR
============================================ */
@@ -1895,90 +1709,6 @@ body.show-desc .bm-desc { display: block; }
display: flex; gap: 8px;
}
/* ============================================
BROWSER BOOKMARK IMPORT MODAL
============================================ */
.bm-import-overlay {
position: fixed; inset: 0; z-index: 9999;
background: rgba(0,0,0,0.6); backdrop-filter: blur(6px);
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none;
transition: opacity 0.25s ease;
}
.bm-import-overlay.active { opacity: 1; pointer-events: all; }
.bm-import-modal {
background: rgba(8,8,16,0.96);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
backdrop-filter: blur(28px);
max-width: 480px; width: 90%;
max-height: 75vh;
display: flex; flex-direction: column;
transform: translateY(12px) scale(0.97);
transition: transform 0.25s ease;
}
.bm-import-overlay.active .bm-import-modal {
transform: translateY(0) scale(1);
}
.bm-import-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 18px; border-bottom: 1px solid var(--border);
font-size: 14px; font-weight: 600;
color: var(--text-primary);
}
.bm-import-close {
background: none; border: none; color: var(--text-muted);
font-size: 20px; cursor: pointer; padding: 0 4px;
transition: color 0.15s;
}
.bm-import-close:hover { color: var(--text-primary); }
.bm-import-info {
padding: 10px 18px;
font-size: 12px; color: var(--text-secondary);
line-height: 1.5;
}
.bm-import-list {
flex: 1; overflow-y: auto; padding: 4px 0;
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
max-height: 45vh;
}
.bm-import-folder {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px; cursor: pointer;
transition: background 0.15s;
font-size: 13px; color: var(--text-primary);
}
.bm-import-folder:hover {
background: rgba(255,255,255,0.04);
}
.bm-import-checkbox {
accent-color: var(--accent);
width: 16px; height: 16px;
cursor: pointer; flex-shrink: 0;
}
.bm-import-folder-name {
flex: 1; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap;
}
.bm-import-folder-meta {
font-size: 11px; color: var(--text-muted);
white-space: nowrap; flex-shrink: 0;
}
.bm-import-footer {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 18px; border-top: 1px solid var(--border);
gap: 8px;
}
/* ============================================
THEME PICKER MODAL
============================================ */
+1 -3
View File
@@ -20,8 +20,6 @@ async function init() {
await Notes.init();
await Calculator.init();
await Timer.init();
await ImageRef.init();
BrowserBookmarkImport.init();
initDataButtons();
Store.checkQuota();
@@ -104,7 +102,7 @@ async function checkBackupReminder() {
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : [];
const data = { version: '1.11.1', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory, timerPresets };
const data = { version: '1.7.0', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory, timerPresets };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
-303
View File
@@ -1,303 +0,0 @@
/* =============================================
HELLION NEWTAB — bookmark-import.js
Direkt-Import von Browser-Lesezeichen
via chrome.bookmarks.getTree() / browser.bookmarks.getTree()
============================================= */
const BrowserBookmarkImport = {
/** Initialisiert den Import-Button */
init() {
const btn = document.getElementById('btnBrowserImport');
const row = document.getElementById('browserImportRow');
if (!btn || !row) return;
// API-Verfuegbarkeit pruefen (nicht vorhanden im normalen Browser-Tab)
const api = this._getApi();
if (!api) {
row.style.display = 'none';
return;
}
btn.addEventListener('click', () => this._openFolderModal());
},
/**
* Gibt die Bookmarks-API zurueck (Chrome oder Firefox)
* @returns {object|null}
*/
_getApi() {
if (typeof chrome !== 'undefined' && chrome.bookmarks) return chrome.bookmarks;
if (typeof browser !== 'undefined' && browser.bookmarks) return browser.bookmarks;
return null;
},
/** Oeffnet das Ordner-Auswahl Modal */
async _openFolderModal() {
const api = this._getApi();
if (!api) return;
let tree;
try {
tree = await api.getTree();
} catch (err) {
await HellionDialog.alert(
'Zugriff auf Browser-Lesezeichen nicht möglich. Stelle sicher, dass die Extension die nötigen Berechtigungen hat.',
{ type: 'warning', title: 'Lesezeichen-Import' }
);
return;
}
const folders = this._extractFolders(tree[0]);
if (folders.length === 0) {
await HellionDialog.alert(
'Keine Lesezeichen-Ordner gefunden.',
{ type: 'warning', title: 'Lesezeichen-Import' }
);
return;
}
this._renderModal(folders);
},
/**
* Extrahiert alle Ordner rekursiv aus dem Bookmark-Baum
* @param {object} node - Bookmark-Tree Node
* @param {number} depth - Einrueckungstiefe
* @returns {Array}
*/
_extractFolders(node, depth) {
if (depth === undefined) depth = 0;
const result = [];
if (!node.children) return result;
for (const child of node.children) {
if (child.children) {
const bookmarkCount = child.children.filter(function(c) { return c.url; }).length;
const subfolderCount = child.children.filter(function(c) { return c.children; }).length;
result.push({
id: child.id,
title: child.title || 'Unbenannt',
depth: depth,
bookmarkCount: bookmarkCount,
subfolderCount: subfolderCount,
node: child
});
const subFolders = this._extractFolders(child, depth + 1);
for (const sf of subFolders) {
result.push(sf);
}
}
}
return result;
},
/**
* Rendert das Ordner-Auswahl Modal
* @param {Array} folders - Liste der Ordner
*/
_renderModal(folders) {
// Overlay
const overlay = document.createElement('div');
overlay.className = 'bm-import-overlay';
overlay.id = 'bmImportOverlay';
const modal = document.createElement('div');
modal.className = 'bm-import-modal';
// Header
const header = document.createElement('div');
header.className = 'bm-import-header';
const title = document.createElement('span');
title.textContent = 'Browser-Lesezeichen importieren';
header.appendChild(title);
const closeBtn = document.createElement('button');
closeBtn.className = 'bm-import-close';
closeBtn.textContent = '\u00D7';
closeBtn.addEventListener('click', () => this._closeModal());
header.appendChild(closeBtn);
modal.appendChild(header);
// Info
const info = document.createElement('div');
info.className = 'bm-import-info';
info.textContent = 'Wähle die Ordner aus, die als Boards importiert werden sollen. Jeder Ordner wird ein eigenes Board.';
modal.appendChild(info);
// Ordner-Liste
const list = document.createElement('div');
list.className = 'bm-import-list';
for (const folder of folders) {
const row = document.createElement('label');
row.className = 'bm-import-folder';
row.style.paddingLeft = (12 + folder.depth * 20) + 'px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'bm-import-checkbox';
checkbox.dataset.folderId = folder.id;
row.appendChild(checkbox);
const label = document.createElement('span');
label.className = 'bm-import-folder-name';
label.textContent = folder.title;
row.appendChild(label);
const meta = document.createElement('span');
meta.className = 'bm-import-folder-meta';
const parts = [];
if (folder.bookmarkCount > 0) {
parts.push(folder.bookmarkCount + ' Link' + (folder.bookmarkCount !== 1 ? 's' : ''));
}
if (folder.subfolderCount > 0) {
parts.push(folder.subfolderCount + ' Ordner');
}
if (parts.length === 0) {
parts.push('leer');
}
meta.textContent = parts.join(', ');
row.appendChild(meta);
list.appendChild(row);
}
modal.appendChild(list);
// Footer
const footer = document.createElement('div');
footer.className = 'bm-import-footer';
const selectAll = document.createElement('button');
selectAll.className = 'btn-secondary';
selectAll.textContent = 'Alle auswählen';
selectAll.addEventListener('click', () => {
const boxes = list.querySelectorAll('.bm-import-checkbox');
const allChecked = Array.from(boxes).every(function(cb) { return cb.checked; });
boxes.forEach(function(cb) { cb.checked = !allChecked; });
selectAll.textContent = allChecked ? 'Alle auswählen' : 'Alle abwählen';
});
footer.appendChild(selectAll);
const importBtn = document.createElement('button');
importBtn.className = 'btn-primary';
importBtn.textContent = 'Importieren';
importBtn.addEventListener('click', () => this._importSelected(folders));
footer.appendChild(importBtn);
modal.appendChild(footer);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Animation
requestAnimationFrame(() => overlay.classList.add('active'));
},
/** Schliesst das Modal */
_closeModal() {
const overlay = document.getElementById('bmImportOverlay');
if (!overlay) return;
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 250);
},
/**
* Importiert die ausgewaehlten Ordner als Boards
* @param {Array} folders - Alle Ordner
*/
async _importSelected(folders) {
const checkboxes = document.querySelectorAll('.bm-import-checkbox:checked');
if (checkboxes.length === 0) {
await HellionDialog.alert(
'Bitte wähle mindestens einen Ordner aus.',
{ type: 'warning', title: 'Lesezeichen-Import' }
);
return;
}
// Bestehende URLs sammeln fuer Duplikat-Erkennung
const existingUrls = new Set();
for (const board of boards) {
for (const bm of board.bookmarks) {
existingUrls.add(bm.url);
}
}
const selectedIds = new Set();
checkboxes.forEach(function(cb) { selectedIds.add(cb.dataset.folderId); });
let totalImported = 0;
let totalSkipped = 0;
let boardsCreated = 0;
for (const folder of folders) {
if (!selectedIds.has(folder.id)) continue;
const bookmarks = [];
for (const child of folder.node.children) {
if (!child.url) continue;
// Nur http/https URLs
try {
const parsed = new URL(child.url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') continue;
} catch (e) {
continue;
}
// Duplikat-Check
if (existingUrls.has(child.url)) {
totalSkipped++;
continue;
}
bookmarks.push({
id: uid(),
title: child.title || child.url,
url: child.url,
desc: ''
});
existingUrls.add(child.url);
totalImported++;
}
if (bookmarks.length === 0) continue;
boards.push({
id: uid(),
title: folder.title,
bookmarks: bookmarks,
blurred: false
});
boardsCreated++;
}
if (boardsCreated > 0) {
await saveBoards();
renderBoards();
}
this._closeModal();
// Ergebnis-Dialog
const lines = [];
lines.push(boardsCreated + ' Board' + (boardsCreated !== 1 ? 's' : '') + ' erstellt');
lines.push(totalImported + ' Lesezeichen importiert');
if (totalSkipped > 0) {
lines.push(totalSkipped + ' Duplikat' + (totalSkipped !== 1 ? 'e' : '') + ' übersprungen');
}
await HellionDialog.alert(
lines.join('\n'),
{ type: 'success', title: 'Import abgeschlossen' }
);
}
};
+1 -1
View File
@@ -13,7 +13,7 @@ function initDataButtons() {
btnExport.addEventListener('click', async () => {
const widgetData = await Store.get('widgetStates');
const data = {
version: '1.11.1',
version: '1.7.0',
exported: new Date().toISOString(),
boards,
settings,
-500
View File
@@ -1,500 +0,0 @@
/* =============================================
HELLION NEWTAB — image-ref.js
Bild-Referenz Widget: Session-only Bildanzeige
mit Canvas API WebP-Konvertierung
============================================= */
const ImageRef = {
MAX_IMAGES: 3,
STORAGE_KEY: 'widgetStates',
SESSION_KEY: 'imageRefData',
/** @type {Array<{id: string, label: string, x: number, y: number, width: number, height: number, open: boolean}>} */
_images: [],
_saveTimer: null,
// ---- STORAGE (persistent: Position/Meta) ----
/**
* Widget-Meta aus persistentem Storage laden
*/
async load() {
const data = await Store.get(this.STORAGE_KEY);
if (data && data.imageRef && Array.isArray(data.imageRef.images)) {
this._images = data.imageRef.images;
}
},
/**
* Widget-Meta persistent speichern
* Bestehende Notes, Calculator, Timer bleiben erhalten
*/
async save() {
const data = await Store.get(this.STORAGE_KEY) || {};
if (data.notes === undefined) data.notes = [];
// Positionen aus WidgetManager aktualisieren
const updated = this._images.map(img => {
const ws = WidgetManager.getState(img.id);
if (ws) {
img.x = ws.x;
img.y = ws.y;
img.width = ws.width;
img.height = ws.height;
img.open = ws.open;
}
return img;
});
data.imageRef = { images: updated };
await Store.set(this.STORAGE_KEY, data);
},
/**
* Debounced Save
*/
_debouncedSave() {
clearTimeout(this._saveTimer);
this._saveTimer = setTimeout(() => this.save(), 500);
},
// ---- SESSION STORAGE (Bilddaten) ----
/**
* Bilddaten in sessionStorage speichern
*/
_saveSession() {
try {
const sessionData = {};
this._images.forEach(img => {
const dataUrl = this._getSessionImage(img.id);
if (dataUrl) sessionData[img.id] = dataUrl;
});
sessionStorage.setItem(this.SESSION_KEY, JSON.stringify(sessionData));
} catch (e) {
console.warn('ImageRef: sessionStorage Write fehlgeschlagen', e);
}
},
/**
* Bilddaten aus sessionStorage laden
* @returns {Object} - { id: dataUrl, ... }
*/
_loadSessionAll() {
try {
const raw = sessionStorage.getItem(this.SESSION_KEY);
if (!raw) return {};
return JSON.parse(raw);
} catch (e) {
console.warn('ImageRef: sessionStorage Read fehlgeschlagen', e);
return {};
}
},
/**
* Einzelnes Bild aus sessionStorage lesen
* @param {string} id
* @returns {string|null}
*/
_getSessionImage(id) {
const all = this._loadSessionAll();
return all[id] || null;
},
/**
* Einzelnes Bild in sessionStorage setzen
* @param {string} id
* @param {string} dataUrl
*/
_setSessionImage(id, dataUrl) {
try {
const all = this._loadSessionAll();
all[id] = dataUrl;
sessionStorage.setItem(this.SESSION_KEY, JSON.stringify(all));
} catch (e) {
console.warn('ImageRef: sessionStorage Write fehlgeschlagen', e);
HellionDialog.alert(
'Bild konnte nicht gespeichert werden. Der Browser-Speicher ist voll.',
{ type: 'danger', title: 'Speicherfehler' }
);
}
},
/**
* Einzelnes Bild aus sessionStorage entfernen
* @param {string} id
*/
_removeSessionImage(id) {
try {
const all = this._loadSessionAll();
delete all[id];
sessionStorage.setItem(this.SESSION_KEY, JSON.stringify(all));
} catch (e) {
console.warn('ImageRef: sessionStorage Remove fehlgeschlagen', e);
}
},
// ---- WIDGET LIFECYCLE ----
/**
* Neues Bild-Widget erstellen (oeffnet File-Dialog)
*/
async create() {
if (!settings.imageRefEnabled) return;
if (this._images.length >= this.MAX_IMAGES) {
await HellionDialog.alert(
'Maximal ' + this.MAX_IMAGES + ' Bild-Widgets gleichzeitig. Schliesse eines um ein neues zu oeffnen.',
{ type: 'warning', title: 'Limit erreicht' }
);
return;
}
// Freie ID finden
const usedIds = new Set(this._images.map(i => i.id));
let slotId = null;
for (let i = 0; i < this.MAX_IMAGES; i++) {
const candidate = 'image_' + i;
if (!usedIds.has(candidate)) {
slotId = candidate;
break;
}
}
if (!slotId) return;
// File-Dialog
const file = await this._pickFile();
if (!file) return;
// Bild verarbeiten
let dataUrl;
try {
dataUrl = await this._processFile(file);
} catch (err) {
await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message,
{ type: 'danger', title: 'Bildfehler' }
);
return;
}
// In sessionStorage speichern
this._setSessionImage(slotId, dataUrl);
// Meta erstellen
const imageData = {
id: slotId,
label: '',
x: 200 + (this._images.length * 40),
y: 120 + (this._images.length * 30),
width: 320,
height: 280,
open: true
};
this._images.push(imageData);
// Widget erstellen
this._createWidget(imageData, dataUrl);
await this.save();
},
/**
* Widget im DOM erstellen
* @param {Object} imageData
* @param {string|null} dataUrl
*/
_createWidget(imageData, dataUrl) {
WidgetManager.create('image', {
id: imageData.id,
title: imageData.label || 'Bild-Referenz',
x: imageData.x,
y: imageData.y,
width: imageData.width,
height: imageData.height,
open: imageData.open !== false
});
const body = WidgetManager.getBody(imageData.id);
if (body) this.renderBody(imageData, body, dataUrl);
},
/**
* Widget geschlossen — Daten aufraeumen
* @param {string} id
*/
async onClose(id) {
this._removeSessionImage(id);
this._images = this._images.filter(img => img.id !== id);
await this.save();
},
// ---- UI RENDERING ----
/**
* Widget-Body rendern
* @param {Object} imageData
* @param {HTMLElement} bodyEl
* @param {string|null} dataUrl
*/
renderBody(imageData, bodyEl, dataUrl) {
bodyEl.textContent = '';
const container = document.createElement('div');
container.className = 'imgref-container';
if (dataUrl) {
// Bild anzeigen
const wrapper = document.createElement('div');
wrapper.className = 'imgref-img-wrapper';
const img = document.createElement('img');
img.className = 'imgref-img';
img.src = dataUrl;
img.alt = imageData.label || 'Bild-Referenz';
wrapper.appendChild(img);
// Bild ersetzen Button
const replaceBtn = document.createElement('button');
replaceBtn.className = 'imgref-replace-btn';
replaceBtn.type = 'button';
replaceBtn.textContent = 'Bild ersetzen';
replaceBtn.addEventListener('click', async () => {
const file = await this._pickFile();
if (!file) return;
try {
const newDataUrl = await this._processFile(file);
this._setSessionImage(imageData.id, newDataUrl);
this.renderBody(imageData, bodyEl, newDataUrl);
} catch (err) {
await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message,
{ type: 'danger', title: 'Bildfehler' }
);
}
});
container.append(wrapper, replaceBtn);
} else {
// Drop-Zone (kein Bild vorhanden)
const dropzone = this._createDropzone(imageData, bodyEl);
container.appendChild(dropzone);
}
// Label-Input
const label = document.createElement('input');
label.className = 'imgref-label';
label.type = 'text';
label.placeholder = 'Beschriftung (optional)';
label.maxLength = 100;
label.value = imageData.label || '';
label.addEventListener('input', () => {
const text = label.value.trim().slice(0, 100);
imageData.label = text;
// Widget-Titel aktualisieren
const entry = WidgetManager._widgets.get(imageData.id);
if (entry) {
const titleEl = entry.el.querySelector('.widget-title-text');
if (titleEl) titleEl.textContent = text || 'Bild-Referenz';
entry.state.title = text || 'Bild-Referenz';
}
this._debouncedSave();
});
container.appendChild(label);
bodyEl.appendChild(container);
},
/**
* Drop-Zone erstellen (fuer leere Widgets / neue Bilder)
* @param {Object} imageData
* @param {HTMLElement} bodyEl
* @returns {HTMLElement}
*/
_createDropzone(imageData, bodyEl) {
const dropzone = document.createElement('div');
dropzone.className = 'imgref-dropzone';
const icon = document.createElement('div');
icon.className = 'imgref-dropzone-icon';
icon.textContent = '\uD83D\uDDBC\uFE0F';
const text = document.createElement('span');
text.textContent = 'Klicken oder Bild hierher ziehen';
dropzone.append(icon, text);
// Klick -> File-Dialog
dropzone.addEventListener('click', async () => {
const file = await this._pickFile();
if (!file) return;
try {
const dataUrl = await this._processFile(file);
this._setSessionImage(imageData.id, dataUrl);
this.renderBody(imageData, bodyEl, dataUrl);
await this.save();
} catch (err) {
await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message,
{ type: 'danger', title: 'Bildfehler' }
);
}
});
// Drag & Drop
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
dropzone.classList.remove('dragover');
});
dropzone.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
dropzone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (!file || !file.type.startsWith('image/')) {
await HellionDialog.alert(
'Bitte eine Bilddatei verwenden (PNG, JPG, WebP, etc.).',
{ type: 'warning', title: 'Kein Bild' }
);
return;
}
try {
const dataUrl = await this._processFile(file);
this._setSessionImage(imageData.id, dataUrl);
this.renderBody(imageData, bodyEl, dataUrl);
await this.save();
} catch (err) {
await HellionDialog.alert(
'Bild konnte nicht geladen werden: ' + err.message,
{ type: 'danger', title: 'Bildfehler' }
);
}
});
return dropzone;
},
// ---- FILE HANDLING ----
/**
* File-Dialog oeffnen
* @returns {Promise<File|null>}
*/
_pickFile() {
return new Promise(resolve => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.addEventListener('change', () => {
resolve(input.files[0] || null);
});
// Cancel erkennen
input.addEventListener('cancel', () => resolve(null));
input.click();
});
},
/**
* Bild per Canvas API zu WebP konvertieren
* @param {File} file
* @returns {Promise<string>} WebP DataURL
*/
_processFile(file) {
return new Promise((resolve, reject) => {
const objectUrl = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
try {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const webpUrl = canvas.toDataURL('image/webp', 0.85);
URL.revokeObjectURL(objectUrl);
resolve(webpUrl);
} catch (err) {
URL.revokeObjectURL(objectUrl);
reject(err);
}
};
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error('Bild konnte nicht geladen werden'));
};
img.src = objectUrl;
});
},
// ---- INIT ----
/**
* ImageRef initialisieren (aus app.js aufgerufen)
*/
async init() {
await this.load();
// Widgets wiederherstellen (nur wenn Feature aktiviert)
if (settings.imageRefEnabled && this._images.length > 0) {
const sessionData = this._loadSessionAll();
this._images.forEach(imageData => {
if (imageData.open !== false) {
const dataUrl = sessionData[imageData.id] || null;
this._createWidget(imageData, dataUrl);
}
});
}
// Close-Event abfangen
const self = this;
const prevClose = WidgetManager.close;
WidgetManager.close = function(id) {
prevClose.call(WidgetManager, id);
// Pruefen ob es ein Image-Widget ist
const isImage = self._images.some(img => img.id === id);
if (isImage) {
self.onClose(id);
}
};
// Minimize-Event abfangen
const prevMinimize = WidgetManager.minimize;
WidgetManager.minimize = async function(id) {
await prevMinimize.call(WidgetManager, id);
const isImage = self._images.some(img => img.id === id);
if (isImage) {
await self.save();
}
};
// Open-Event abfangen
const prevOpen = WidgetManager.openWidget;
WidgetManager.openWidget = async function(id) {
await prevOpen.call(WidgetManager, id);
const imgData = self._images.find(img => img.id === id);
if (imgData) {
const body = WidgetManager.getBody(id);
if (body && body.children.length === 0) {
const dataUrl = self._getSessionImage(id);
self.renderBody(imgData, body, dataUrl);
}
await self.save();
}
};
}
};
-5
View File
@@ -53,9 +53,6 @@ const Notes = {
if (existing && existing.timer) {
saveData.timer = existing.timer;
}
if (existing && existing.imageRef) {
saveData.imageRef = existing.imageRef;
}
await Store.set(this.STORAGE_KEY, saveData);
},
@@ -530,8 +527,6 @@ const Notes = {
Calculator.toggle();
} else if (action === 'timer') {
Timer.toggle();
} else if (action === 'image-ref') {
ImageRef.create();
} else if (action === 'notebook') {
this.openNotebook();
}
+11 -67
View File
@@ -24,20 +24,18 @@ const Onboarding = {
},
{
hero: '\uD83C\uDFA8',
title: '11 handgefertigte Themes',
text: 'Klicke auf den \u201ETheme\u201C Button im Header um dein Theme zu w\u00E4hlen. Jedes hat seinen eigenen Stil und Farbpalette.',
title: '8 handgefertigte Themes',
text: 'Klicke auf den „Theme" Button im Header um dein Theme zu wählen. Jedes hat seinen eigenen Stil und Farbpalette.',
showThemes: true
},
{
hero: '\uD83E\uDDF0',
title: 'Widget-Toolbar',
hero: '\u26A1',
title: 'Weitere Features',
features: [
'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'
'Suchleiste mit Google, DuckDuckGo oder Bing',
'Widget-Toolbar rechts \u2014 Notes und Checklisten erstellen',
'Notebook-Sidebar \u00FCber den \u201ENote\u201C Button oder die Toolbar',
'Funktioniert komplett offline \u2014 alles lokal gespeichert'
]
},
{
@@ -45,16 +43,10 @@ const Onboarding = {
title: 'Backups nicht vergessen!',
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.'
},
{
hero: '\uD83C\uDFAE',
title: 'Gaming Starter Board',
text: 'Spielst du Games wie Satisfactory, Factorio oder Star Citizen? Ich kann ein Board mit n\u00FCtzlichen Community-Links anlegen.',
interactive: 'gaming-board'
},
{
hero: '\uD83D\uDE80',
title: 'Bereit!',
text: 'Erstelle dein erstes Board mit \u201E+ Board\u201C oder importiere deine Browser-Lesezeichen \u00FCber den Import-Button im Header. Viel Spa\u00DF!'
text: 'Klicke auf \u201E+ Board\u201C um dein erstes Board zu erstellen, oder nutze den \u201EImport\u201C Button im Header um deine Browser-Lesezeichen zu importieren.'
}
],
@@ -127,7 +119,7 @@ const Onboarding = {
if (slide.showThemes) {
const grid = document.createElement('div');
grid.className = 'onboarding-theme-grid';
const themeNames = ['Nebula', 'Crescent', 'Event Horizon', 'Merchantman', 'Julia & Jin', 'SC Sunset', 'Hellion HUD', 'Hellion Energy', 'Satisfactory', 'Avorion', 'Hellion Stealth'];
const themeNames = ['Nebula', 'Crescent', 'Event Horizon', 'Merchantman', 'Julia & Jin', 'SC Sunset', 'Hellion HUD', 'Hellion Energy'];
themeNames.forEach(name => {
const chip = document.createElement('div');
chip.className = 'onboarding-theme-chip';
@@ -168,27 +160,7 @@ const Onboarding = {
nav.appendChild(backBtn);
}
if (slide.interactive === 'gaming-board') {
// Interaktive Slide: Zwei Buttons statt "Weiter"
const noBtn = document.createElement('button');
noBtn.className = 'btn-secondary';
noBtn.textContent = 'Nein danke';
noBtn.addEventListener('click', () => {
this.currentSlide++;
this._render();
});
const yesBtn = document.createElement('button');
yesBtn.className = 'btn-primary';
yesBtn.textContent = 'Ja, gerne';
yesBtn.addEventListener('click', async () => {
await this._createGamingBoard();
this.currentSlide++;
this._render();
});
nav.append(noBtn, yesBtn);
} else if (isLast) {
if (isLast) {
const startBtn = document.createElement('button');
startBtn.className = 'btn-primary';
startBtn.textContent = 'Los geht\u2019s!';
@@ -209,34 +181,6 @@ const Onboarding = {
modal.appendChild(footer);
},
/**
* Gaming Starter Board erstellen
* Vorbefuelltes Board mit Community-Links fuer Factory/Space Games
*/
async _createGamingBoard() {
const gamingBoard = {
id: uid(),
title: '\uD83C\uDFAE Gaming',
bookmarks: [
{ id: uid(), title: 'Satisfactory Wiki', url: 'https://satisfactory.wiki.gg', desc: '' },
{ id: uid(), title: 'Satisfactory Calculator', url: 'https://satisfactorytools.com', desc: '' },
{ id: uid(), title: 'Factorio Wiki', url: 'https://wiki.factorio.com', desc: '' },
{ id: uid(), title: 'Factorio Cheatsheet', url: 'https://factoriocheatsheet.com', desc: '' },
{ id: uid(), title: 'Avorion Wiki', url: 'https://wiki.avorion.net', desc: '' },
{ id: uid(), title: 'Minecraft Wiki', url: 'https://minecraft.wiki', desc: '' },
{ id: uid(), title: 'Modrinth (Mods)', url: 'https://modrinth.com', desc: '' },
{ id: uid(), title: 'Star Citizen Wiki', url: 'https://starcitizen.tools', desc: '' },
{ id: uid(), title: 'UEX Corp (Trading)', url: 'https://uexcorp.space', desc: '' },
{ id: uid(), title: 'Hellion TradeCenter', url: 'https://hellion-initiative.online/tradecenter', desc: 'Trade Center f\u00FCr Star Citizen' }
],
blurred: false
};
boards.push(gamingBoard);
await saveBoards();
renderBoards();
},
/** Keyboard-Navigation */
_bindKeyboard() {
this._keyHandler = (e) => {
+2 -15
View File
@@ -25,7 +25,7 @@ function closeThemeModal() {
// ---- ACCORDION ----
function initAccordion() {
const defaultOpen = new Set(['widgets']);
const defaultOpen = new Set(['appearance', 'behavior', 'widgets', 'data', 'help']);
const sections = document.querySelectorAll('.settings-section[data-section]');
sections.forEach(section => {
@@ -71,13 +71,6 @@ function applySettings() {
const showSearchEl = document.getElementById('settingShowSearch');
if (showSearchEl) showSearchEl.checked = settings.showSearch;
// Image-Ref Toggle
if (settings.imageRefEnabled === undefined) settings.imageRefEnabled = false;
const imgRefCheckbox = document.getElementById('settingImageRef');
if (imgRefCheckbox) imgRefCheckbox.checked = settings.imageRefEnabled;
const imgRefBtn = document.querySelector('[data-action="image-ref"]');
if (imgRefBtn) imgRefBtn.classList.toggle('hidden', !settings.imageRefEnabled);
// Toolbar-Position
document.body.classList.toggle('toolbar-left', settings.toolbarPos === 'left');
const toolbarPosEl = document.getElementById('settingToolbarPos');
@@ -134,11 +127,6 @@ function bindSettingsEvents() {
settingShowSearch: v => {
settings.showSearch = v;
document.getElementById('searchBarWrapper').classList.toggle('hidden', !v);
},
settingImageRef: v => {
settings.imageRefEnabled = v;
const imgBtn = document.querySelector('[data-action="image-ref"]');
if (imgBtn) imgBtn.classList.toggle('hidden', !v);
}
};
@@ -216,8 +204,7 @@ function bindSettingsEvents() {
boards = [];
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
showSearch: true, searchEngine: 'google', toolbarPos: 'right',
imageRefEnabled: false };
showSearch: true, searchEngine: 'google', toolbarPos: 'right' };
await saveBoards();
await saveSettings();
applySettings();
+1 -2
View File
@@ -16,8 +16,7 @@ let settings = {
theme: 'nebula',
showSearch: true,
searchEngine: 'google',
toolbarPos: 'right',
imageRefEnabled: false
toolbarPos: 'right'
};
function uid() {
+7 -10
View File
@@ -4,17 +4,14 @@
============================================= */
const THEMES = {
'nebula': { bg: 'assets/themes/bg-nebula.webp' },
'crescent': { bg: 'assets/themes/bg-crescent.webp' },
'event-horizon': { bg: 'assets/themes/bg-event-horizon.webp' },
'nebula': { bg: 'assets/themes/bg-nebula.jpg' },
'crescent': { bg: 'assets/themes/bg-crescent.jpg' },
'event-horizon': { bg: 'assets/themes/bg-event-horizon.jpg' },
'merchantman': { bg: 'assets/themes/bg-merchantman.webp' },
'julia-jin': { bg: 'assets/themes/bg-julia-jin.webp' },
'sc-sunset': { bg: 'assets/themes/bg-sc-sunset.webp' },
'hellion-hud': { bg: 'assets/themes/bg-hellion-hud.webp' },
'hellion-energy': { bg: 'assets/themes/bg-hellion-energy.webp' },
'satisfactory': { bg: 'assets/themes/bg-satisfactory.webp' },
'avorion': { bg: 'assets/themes/bg-avorion.webp' },
'hellion-stealth': { bg: 'assets/themes/bg-scPolaris.webp' }
'julia-jin': { bg: 'assets/themes/bg-julia-jin.png' },
'sc-sunset': { bg: 'assets/themes/bg-sc-sunset.jpg' },
'hellion-hud': { bg: 'assets/themes/bg-hellion-hud.png' },
'hellion-energy': { bg: 'assets/themes/bg-hellion-energy.jpg' }
};
function applyTheme(themeName, skipBgOverride) {