feat(app): Onboarding, Settings-Redesign und Docs für v1.9.0
- Onboarding mit Widget-Toolbar Slide und Gaming Starter Board - Settings in Darstellung-Modal und schlankes Settings-Panel - About-Block als fixierten Footer im Settings-Panel - Dropdown-Optionen an Theme-Farben anpassen - Projekt-Dokumentation (Architektur, Widget-Schema, Patterns) - Firefox Update-URL für Store-Veröffentlichung - Versions-Bump auf 1.9.0 in allen Manifests
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
|
||||
ko_fi: hellionmedia
|
||||
@@ -16,6 +16,10 @@ 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ⬡ Hellion Dashboard v1.5.2
|
||||
# ⬡ Hellion Dashboard v1.9.0
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,310 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,330 @@
|
||||
# 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,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hellion NewTab",
|
||||
"version": "1.5.2",
|
||||
"version": "1.9.0",
|
||||
"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,6 +18,7 @@
|
||||
"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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hellion NewTab",
|
||||
"version": "1.5.2",
|
||||
"version": "1.9.0",
|
||||
"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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hellion Dashboard (GX Native)",
|
||||
"version": "1.5.2",
|
||||
"version": "1.9.0",
|
||||
"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",
|
||||
|
||||
+93
-126
@@ -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="Theme wählen">
|
||||
<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>
|
||||
Theme
|
||||
Darstellung
|
||||
</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>
|
||||
@@ -105,84 +105,11 @@
|
||||
<div class="panel-overlay" id="settingsOverlay"></div>
|
||||
<aside class="settings-panel" id="settingsPanel">
|
||||
<div class="panel-header">
|
||||
<span>Settings</span>
|
||||
<span>Einstellungen</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">
|
||||
@@ -193,7 +120,7 @@
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Toolbar-Position</span>
|
||||
<span class="setting-desc">Widget-Toolbar links oder rechts</span>
|
||||
<span class="setting-desc">Widget-Toolbar links oder rechts anzeigen</span>
|
||||
</div>
|
||||
<select class="select-input" id="settingToolbarPos">
|
||||
<option value="right" selected>Rechts</option>
|
||||
@@ -213,38 +140,28 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DATA -->
|
||||
<!-- DATEN & HILFE -->
|
||||
<section class="settings-section" data-section="data">
|
||||
<button class="settings-section-title" type="button">
|
||||
<span class="section-chevron">▸</span>
|
||||
DATA
|
||||
DATEN & HILFE
|
||||
</button>
|
||||
<div class="section-content">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Export Boards</span>
|
||||
<span class="setting-desc">Alle Boards als JSON sichern</span>
|
||||
<span class="setting-label">Backup exportieren</span>
|
||||
<span class="setting-desc">Alle Boards, Notes und Einstellungen 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">Import Boards</span>
|
||||
<span class="setting-label">Backup importieren</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>
|
||||
</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>
|
||||
@@ -255,16 +172,30 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ABOUT / IMPRESSUM -->
|
||||
<section class="settings-section" data-section="about">
|
||||
<button class="settings-section-title" type="button">
|
||||
<!-- DANGER ZONE -->
|
||||
<section class="settings-section" data-section="danger">
|
||||
<button class="settings-section-title danger" type="button">
|
||||
<span class="section-chevron">▸</span>
|
||||
ABOUT
|
||||
DANGER ZONE
|
||||
</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.5.2 · by Hellion Online Media</div>
|
||||
<div class="about-version">Version 1.9.0 · by Hellion Online Media</div>
|
||||
|
||||
<div class="about-links">
|
||||
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
|
||||
@@ -328,33 +259,13 @@
|
||||
</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>Theme wählen</span>
|
||||
<span>Darstellung</span>
|
||||
<button class="btn-close" id="btnCloseTheme">✕</button>
|
||||
</div>
|
||||
<div class="theme-grid">
|
||||
@@ -400,27 +311,83 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-modal-section">
|
||||
<h3 class="settings-section-title">BACKGROUND</h3>
|
||||
<h3 class="settings-section-title">HINTERGRUND</h3>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Background image URL</span>
|
||||
<span class="setting-desc">Custom wallpaper URL</span>
|
||||
<span class="setting-label">Bild-URL</span>
|
||||
<span class="setting-desc">Eigenes Hintergrundbild per URL</span>
|
||||
</div>
|
||||
<button class="btn-small" id="btnChangeBg">Change</button>
|
||||
<button class="btn-small" id="btnChangeBg">Ändern</button>
|
||||
</div>
|
||||
<div class="setting-row hidden" id="bgInputRow">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Background file upload</span>
|
||||
<span class="setting-desc">Use a local image as background</span>
|
||||
<span class="setting-label">Datei hochladen</span>
|
||||
<span class="setting-desc">Lokales Bild als Hintergrund verwenden</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>
|
||||
|
||||
|
||||
+9
-2
@@ -537,7 +537,13 @@ 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; }
|
||||
.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; }
|
||||
|
||||
.settings-section { margin-bottom: 4px; }
|
||||
.settings-section-title {
|
||||
@@ -639,11 +645,12 @@ body.show-desc .bm-desc { display: block; }
|
||||
|
||||
/* INPUTS */
|
||||
.select-input {
|
||||
padding: 5px 8px; background: rgba(255,255,255,0.06);
|
||||
padding: 5px 8px; background: var(--bg-board, 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 {
|
||||
|
||||
+1
-1
@@ -103,7 +103,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.7.0', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory, timerPresets };
|
||||
const data = { version: '1.9.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');
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ function initDataButtons() {
|
||||
btnExport.addEventListener('click', async () => {
|
||||
const widgetData = await Store.get('widgetStates');
|
||||
const data = {
|
||||
version: '1.7.0',
|
||||
version: '1.9.0',
|
||||
exported: new Date().toISOString(),
|
||||
boards,
|
||||
settings,
|
||||
|
||||
+65
-9
@@ -25,17 +25,19 @@ const Onboarding = {
|
||||
{
|
||||
hero: '\uD83C\uDFA8',
|
||||
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.',
|
||||
text: 'Klicke auf den \u201ETheme\u201C Button im Header um dein Theme zu w\u00E4hlen. Jedes hat seinen eigenen Stil und Farbpalette.',
|
||||
showThemes: true
|
||||
},
|
||||
{
|
||||
hero: '\u26A1',
|
||||
title: 'Weitere Features',
|
||||
hero: '\uD83E\uDDF0',
|
||||
title: 'Widget-Toolbar',
|
||||
features: [
|
||||
'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'
|
||||
'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'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -43,10 +45,16 @@ 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: '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.'
|
||||
text: 'Erstelle dein erstes Board mit \u201E+ Board\u201C oder importiere deine Browser-Lesezeichen \u00FCber den Import-Button im Header. Viel Spa\u00DF!'
|
||||
}
|
||||
],
|
||||
|
||||
@@ -160,7 +168,27 @@ const Onboarding = {
|
||||
nav.appendChild(backBtn);
|
||||
}
|
||||
|
||||
if (isLast) {
|
||||
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) {
|
||||
const startBtn = document.createElement('button');
|
||||
startBtn.className = 'btn-primary';
|
||||
startBtn.textContent = 'Los geht\u2019s!';
|
||||
@@ -181,6 +209,34 @@ 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) => {
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ function closeThemeModal() {
|
||||
|
||||
// ---- ACCORDION ----
|
||||
function initAccordion() {
|
||||
const defaultOpen = new Set(['appearance', 'behavior', 'widgets', 'data', 'help']);
|
||||
const defaultOpen = new Set(['widgets']);
|
||||
const sections = document.querySelectorAll('.settings-section[data-section]');
|
||||
|
||||
sections.forEach(section => {
|
||||
|
||||
Reference in New Issue
Block a user