Compare commits

...

24 Commits

Author SHA1 Message Date
JonKazama-Hellion 86f5644cd5 docs(readme): auf v2.4.0 aktualisiert (Version + Features: Palette/Trash/Quick-Save/freies Layout/Theme-Builder/Hintergrund)
Code Quality / Validate Extension (push) Successful in 5s
Security / scan (push) Successful in 18s
2026-06-15 08:33:33 +02:00
JonKazama-Hellion 4d1ca1bc7e release: v2.4.0
Code Quality / Validate Extension (push) Successful in 6s
Security / scan (push) Successful in 20s
Release / Build & Release (push) Successful in 4s
2026-06-15 08:11:38 +02:00
JonKazama-Hellion 083e78e693 merge: v2.4.0 theme builder into development 2026-06-15 08:11:12 +02:00
JonKazama-Hellion 0001de7dd7 chore(release): Version-Bump 2.4.0 (6 Stellen) + CHANGELOG 2026-06-15 08:10:43 +02:00
JonKazama-Hellion c985a531ef fix(theme): bgLayer beim Custom-Wechsel ohne eigenes Bild leeren
Preset->Custom liess das alte Preset-Hintergrundbild im bgLayer haengen,
weil applyCustomTheme den bgLayer nie anfasste. Jetzt wird er geleert,
wenn keine gueltige bgUrl gesetzt ist, sodass --bg-primary (Solid) durchscheint.
2026-06-15 04:30:51 +02:00
JonKazama-Hellion 2af52fc46d feat(theme): Verdrahtung Custom-Theme (applySettings, selectThemeCard, Picker, Reset, Modal-Sync) 2026-06-15 04:00:59 +02:00
JonKazama-Hellion 1bd2cbb9ad feat(theme): applyCustomTheme/clearCustomTheme/syncCustomPickers + Hex-Validierung + WCAG-Kontrast 2026-06-15 03:31:25 +02:00
JonKazama-Hellion d305d37da5 feat(theme): Eigenes-Kachel + 6-Picker-Panel im Theme-Modal 2026-06-15 03:03:29 +02:00
JonKazama-Hellion 96d4eaa8a1 feat(theme): [data-theme=custom]-Block + Theme-Builder-Panel-Styling 2026-06-15 02:34:42 +02:00
JonKazama-Hellion 22e74d41bc feat(theme): i18n-Keys für Theme-Builder + Onboarding-Wortlaut entschaerft 2026-06-15 02:06:38 +02:00
JonKazama-Hellion d0feddbda0 feat(theme): customTheme-Default im State + Reset-Literal 2026-06-15 01:38:55 +02:00
JonKazama-Hellion 9beeec3182 feat(theme): eigenes Hintergrundbild um https-URLs und Quota-Schutz erweitern
- isValidBgUrl akzeptiert jetzt https:// zusätzlich zu data:/blob: (http bleibt
  ausgeschlossen wegen Mixed-Content)
- CSP img-src 'self' https: data: blob: in allen 3 Manifesten, damit remote
  Hintergründe deterministisch laden statt still am CSP-Default zu haengen
- Upload-Bilder werden vor dem Speichern per Canvas auf die Bildschirmkante
  (max 2560px) verkleinert und als WebP re-kodiert -> schont chrome.storage.local
- URL-Feld: Platzhalter lokalisierbar (data-i18n-placeholder) + Tracking-Hinweis,
  dass ein per URL geladenes Bild bei jedem Oeffnen vom fremden Server kommt
- i18n DE/EN: bg_url.desc + bg_invalid_url an https angepasst, 2 neue Keys
2026-06-14 21:34:34 +02:00
JonKazama-Hellion 42e3cf0dec ci(release): Release via Gitea-API (curl) statt go-basierter release-action
Code Quality / Validate Extension (push) Successful in 5s
Release / Build & Release (push) Successful in 6s
Security / scan (push) Successful in 42s
Die gitea.com/actions/release-action ist 'using: go' und scheitert auf dem Forge-Runner
mit exit 127 — act_runner v0.6.1 bekommt die go-Action weder im Job-Image noch im Runner
kompiliert ('go: executable file not found'). Der Schritt legt das Release jetzt per curl
gegen die Gitea-API an und laedt die Assets hoch, idempotent (vorhandenes Release/Assets
werden wiederverwendet bzw. ersetzt). Laeuft als normaler run-Step im Job-Image und ist
damit unabhaengig von go-Toolchain, Action-Cache und @main-Drift.
2026-06-14 21:05:36 +02:00
JonKazama-Hellion 8c509647da merge: v2.3.0 bookmark comfort features into development 2026-06-14 20:22:11 +02:00
JonKazama-Hellion 2877edee69 release: v2.3.0
Code Quality / Validate Extension (push) Successful in 5s
Security / scan (push) Successful in 19s
Release / Build & Release (push) Failing after 12s
2026-06-14 20:22:11 +02:00
JonKazama-Hellion d041c66dfb feat(layout): Board-Position per Lock-Button fixieren
Neuer Pin-Button (custom SVG, kein Emoji) im Board-Header sperrt die Position eines
Boards. Bei gesperrtem Board (.board.locked):
- der Drag-Handle wird per CSS ausgeblendet (Flos Wunsch: Handle weg statt nur inaktiv),
- ein zweiter Guard in drag.js onDown verweigert zusaetzlich jeden Drag.
Schuetzt vor versehentlichem Verschieben (ergaenzt den 3px-Bewegungs-Schwellwert). locked
wird wie blurred persistiert, im Export/Import durchgereicht und mit ins Trash-Board geklont.
i18n DE/EN ergaenzt.
2026-06-14 20:18:00 +02:00
JonKazama-Hellion 520a062049 fix(quick-save): mid-drag gedrainter Quick-Save wird nach Drag-Ende gerendert
Der Drain laesst renderBoards() aus, solange ein Board- oder Bookmark-Drag laeuft
(replaceChildren wuerde den Drag abreissen) — holte den Render danach aber nie nach,
sodass der gespeicherte Eintrag bis zu einem unabhaengigen Fremd-Render unsichtbar blieb.
Der ausgelassene Render wird jetzt gemerkt (_renderDeferredByDrag) und drag.js ruft am
Ende jedes Drags (onUp/onCancel/Bookmark-dragend) flushQuickSaveRenderIfDeferred nach.
Idempotent: ohne ausstehenden Render kein Extra-Render bei normalen Drags.
2026-06-14 19:55:18 +02:00
JonKazama-Hellion 327bcd3385 fix(quick-save): Drain idempotent (srcId-Dedup) + isSafeUrl-Gate
Zwei Befunde aus der Integrations-Review:
- Race: der Page-Drain und der Worker machen je ein read-modify-write auf
  'quicksave_pending' ohne kontextuebergreifende Atomizitaet. Ein Worker-Append im
  await-Fenster des Drains konnte einen bereits gedrainten Eintrag in der Queue belassen,
  den ein Folge-Drain erneut in die Inbox schrieb (Duplikat). Jede eingespielte Bookmark
  traegt jetzt die Pending-id als srcId; ein erneut auftauchender Eintrag wird uebersprungen
  statt doppelt eingefuegt. boards-Write bleibt vor der Queue-Bereinigung -> kein Verlust.
- Validierung: der Drain hat e.url ohne isSafeUrl gepusht, anders als jeder andere
  Bookmark-Schreibpfad. isSafeUrl (jetzt im DOM-freien quicksave-core, http/https/ftp)
  filtert unsichere/leere Protokolle vor dem Schreiben ins Board.
2026-06-14 19:53:38 +02:00
JonKazama-Hellion 530196ddf7 fix(trash): Import-Cap verdraengt keine lokalen Sole-Copies mehr
Beim Trash-Import sortierte combined.sort+slice(-N) rein nach deletedAt: brachte ein
Backup neuere Eintraege mit, fielen aeltere LOKALE Eintraege aus dem Cap — und die sind
die einzige Kopie der geloeschten Daten (Datenverlust). Jetzt haben lokale Eintraege
Vorrang (alle behalten, sind bereits auf TRASH_MAX_ENTRIES gekappt), Importe fuellen nur
den Rest mit den neuesten auf.
2026-06-14 19:52:16 +02:00
JonKazama-Hellion 17eac64683 fix(layout): geblurrtes Board wieder verschiebbar (Drag-Handle ueber Blur-Overlay)
Das .board-blur-overlay (position:absolute; inset:0; z-index:5) lag im geblurrten
Zustand ueber dem Drag-Handle und schluckte den pointerdown, sodass ein geblurrtes
Board nicht mehr per Handle verschoben werden konnte (stattdessen enthuellte der Klick
es). Der Handle bekommt jetzt position:relative + z-index:6 und liegt damit ueber dem
Overlay; Drag funktioniert, Klick auf den Rest des Boards enthuellt weiterhin.
2026-06-14 19:51:32 +02:00
JonKazama-Hellion 1d17f4d11f fix(layout): Board-Handle-Klick ohne Bewegung ueberschreibt board.pos nicht mehr
Ein reiner Klick/Tap auf den Drag-Handle (ohne echtes Verschieben) hat in onUp den
gegen die Viewport geclampten --board-x/y-Wert zurueckgelesen und als board.pos
persistiert. Bei einem off-screen geclampten Board (nach Fenster-Verkleinerung oder
Import von breiterem Screen) zerstoerte das die wahre Position. Jetzt zaehlt erst eine
Bewegung > 3px als Drag; ohne Bewegung bleibt board.pos unangetastet.
2026-06-14 19:51:15 +02:00
JonKazama-Hellion b3288b47eb docs: add 2.3.0 changelog entry 2026-06-14 15:22:37 +02:00
JonKazama-Hellion 84976f5a10 ci: assert background/action + activeTab/commands in Chrome and Firefox manifests 2026-06-14 15:21:46 +02:00
JonKazama-Hellion 5b18bed9b5 chore: bump version to 2.3.0 across all bump targets 2026-06-14 15:20:07 +02:00
17 changed files with 531 additions and 77 deletions
+10
View File
@@ -45,6 +45,11 @@ jobs:
assert m.get('name'), 'Chrome: Name fehlt'
assert m.get('version'), 'Chrome: Version fehlt'
assert 'storage' in m.get('permissions', []), 'Chrome: Storage Permission fehlt'
assert 'activeTab' in m.get('permissions', []), 'Chrome: activeTab Permission fehlt (Quick Save v2.3)'
assert 'background' in m, 'Chrome: background-Key fehlt (Service Worker v2.3)'
assert 'service_worker' in m.get('background', {}), 'Chrome: background.service_worker fehlt'
assert isinstance(m.get('commands'), dict) and 'quick-save' in m['commands'], 'Chrome: commands.quick-save fehlt (Quick Save v2.3)'
assert 'action' in m, 'Chrome: action-Key fehlt (Badge-Bestätigung v2.3)'
print('manifest.json (V3) OK — Version:', m['version'])
with open('manifest.firefox.json') as f:
@@ -52,6 +57,11 @@ jobs:
assert mf.get('manifest_version') == 3, 'Firefox: Manifest V3 erwartet'
assert mf['version'] == m['version'], 'Firefox: Version stimmt nicht mit Chrome überein!'
assert 'browser_specific_settings' in mf, 'Firefox: browser_specific_settings fehlt'
assert 'activeTab' in mf.get('permissions', []), 'Firefox: activeTab Permission fehlt (Quick Save v2.3)'
assert 'background' in mf, 'Firefox: background-Key fehlt (Event-Page v2.3)'
assert 'scripts' in mf.get('background', {}), 'Firefox: background.scripts fehlt (Event-Page, kein service_worker)'
assert isinstance(mf.get('commands'), dict) and 'quick-save' in mf['commands'], 'Firefox: commands.quick-save fehlt (Quick Save v2.3)'
assert 'action' in mf, 'Firefox: action-Key fehlt (Badge-Bestätigung v2.3)'
print('manifest.firefox.json (V3) OK — Version:', mf['version'])
with open('manifest.opera.json') as f:
+41 -22
View File
@@ -77,28 +77,47 @@ jobs:
sha256sum *.zip > checksums-sha256.txt
cat checksums-sha256.txt
# Gitea-native Release-Action. Legt das Release an, falls der Tag noch
# keins hat, oder aktualisiert das bestehende und haengt die Assets an.
# Der auto-injizierte GITHUB_TOKEN auf Gitea Actions hat Gitea-API-Scope
# und reicht fuer Release-Write.
- name: Attach to Gitea release
uses: https://gitea.com/actions/release-action@main
with:
files: |-
dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip
dist/hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip
dist/hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip
dist/checksums-sha256.txt
api_key: ${{ secrets.GITHUB_TOKEN }}
body: |
## Hellion NewTab ${{ steps.version.outputs.tag }}
# Release per Gitea-API (curl), NICHT via gitea.com/actions/release-action: die ist `using: go`
# und stirbt auf diesem Runner mit exit 127 ("go not found"), weil act_runner v0.6.1 die go-Action
# weder im Job-Image noch im Runner kompiliert bekommt. curl + python3 sind im Job-Image vorhanden
# und laufen als normaler Step -> unabhaengig von go-Toolchain, Action-Cache und @main-Drift.
# GITHUB_API_URL/GITHUB_REPOSITORY/GITHUB_TOKEN injiziert Gitea Actions automatisch.
- name: Create release & upload assets (Gitea API)
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
API="${GITHUB_API_URL:-https://gitea.hellion-forge.cloud/api/v1}"
REPO="${GITHUB_REPOSITORY}"
AUTH="Authorization: token ${GITEA_TOKEN}"
### Installation
- **Chrome / Edge / Brave / Vivaldi:** `hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip`
- **Firefox:** `hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip`
- **Opera / Opera GX:** `hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip`
# Release-Request-JSON (Body inkl. Installationshinweise) als python-Einzeiler bauen
# (mehrzeilig wuerde den YAML-run-Block brechen: Zeilen auf Spalte 0).
REQ=$(python3 -c 'import json,os; t=os.environ["TAG"]; body="## Hellion NewTab "+t+"\n\n### Installation\n- **Chrome / Edge / Brave / Vivaldi:** `hellion-newtab-"+t+"-chrome.zip`\n- **Firefox:** `hellion-newtab-"+t+"-firefox.zip`\n- **Opera / Opera GX:** `hellion-newtab-"+t+"-opera.zip`\n\nVollstaendige Installationsanleitung siehe README.\n\n### Checksums\n`checksums-sha256.txt` zum Verifizieren der Dateiintegritaet."; print(json.dumps({"tag_name": t, "name": "Hellion NewTab "+t, "body": body}))')
Vollstaendige Installationsanleitung siehe README.
# Idempotent: existierendes Release zum Tag wiederverwenden, sonst anlegen.
REL_ID=$(curl -sf -H "$AUTH" "$API/repos/$REPO/releases/tags/$TAG" \
| python3 -c 'import sys,json;print(json.load(sys.stdin).get("id",""))' 2>/dev/null || true)
if [ -z "$REL_ID" ]; then
REL_ID=$(printf '%s' "$REQ" \
| curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" -d @- "$API/repos/$REPO/releases" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
echo "Release angelegt: $REL_ID"
else
echo "Release existiert bereits, wiederverwenden: $REL_ID"
fi
### Checksums
`checksums-sha256.txt` zum Verifizieren der Dateiintegritaet.
# Vorhandene gleichnamige Assets entfernen (idempotent bei Re-Runs), dann hochladen.
EXIST=$(curl -sf -H "$AUTH" "$API/repos/$REPO/releases/$REL_ID/assets" 2>/dev/null || echo '[]')
for f in dist/hellion-newtab-$TAG-chrome.zip dist/hellion-newtab-$TAG-firefox.zip dist/hellion-newtab-$TAG-opera.zip dist/checksums-sha256.txt; do
name=$(basename "$f")
aid=$(printf '%s' "$EXIST" | NAME="$name" python3 -c 'import sys,json,os;n=os.environ["NAME"];print(next((a["id"] for a in json.load(sys.stdin) if a.get("name")==n), ""))' 2>/dev/null || true)
if [ -n "$aid" ]; then
echo "ersetze vorhandenes Asset $name (id $aid)"
curl -sf -X DELETE -H "$AUTH" "$API/repos/$REPO/releases/$REL_ID/assets/$aid" >/dev/null || true
fi
echo "Upload $name ..."
curl -sf -X POST -H "$AUTH" -F "attachment=@$f" "$API/repos/$REPO/releases/$REL_ID/assets?name=$name" >/dev/null
done
echo "Release $TAG fertig: alle Assets hochgeladen."
+27
View File
@@ -6,6 +6,33 @@ All notable changes per version. Format based on [Keep a Changelog](https://keep
---
## [2.4.0] — 2026-06-15
### Added
- **Custom theme builder** — A new "Custom" tile in the theme picker opens an inline panel with six colour pickers (accent, background, board surface, and three text levels). Colours apply live to the dashboard; the accent drives the derived glow, border and toggle tints via `color-mix`. A non-blocking WCAG contrast indicator flags hard-to-read text/background combinations without preventing the choice. The custom theme persists across reloads and can be combined with a custom background image. A reset button returns the pickers to neutral defaults. New DE/EN i18n strings; the `<input type="color">` pickers are labelled for accessibility.
- **Custom background via https URL** — The background URL field now accepts `https://` images in addition to local uploads (http stays out to avoid mixed content). A privacy note explains that a URL-loaded image is fetched from the remote server on every new tab.
### Changed
- Uploaded background images are downscaled (to the longest screen edge, capped at 2560px) and re-encoded as WebP before storage, to protect the `chrome.storage.local` quota.
- The extension-page CSP gains `img-src 'self' https: data: blob:` so https and data-URL backgrounds load deterministically instead of relying on the browser default.
- Onboarding slide 3 wording no longer hard-codes a fixed theme count.
---
## [2.3.0] — 2026-06-14
### Added
- **Command Palette (Ctrl+K)** — Overlay that live-filters bookmarks (title and URL) and board names from the keyboard. Arrow-key navigation, Enter opens the match, Escape closes. Read-only navigation, separate from the web search bar. Combobox/listbox ARIA pattern with focus trap and focus return. New DE/EN i18n strings.
- **Trash** — Deleted bookmarks and boards move to a 30-day trash instead of vanishing. Restore or permanently remove them from a new Settings section; entries older than 30 days are cleaned up automatically. Stored under its own storage key with a hard size cap so it cannot exhaust the storage quota.
- **Quick Save** — A global keyboard shortcut (default Alt+Shift+S, configurable in the browser shortcut settings) saves the current tab into a fixed Inbox board from any page. Backed by a background worker (service worker on Chrome/Opera, event page on Firefox) that appends to a dedicated pending queue, which the dashboard drains into the Inbox — separate write domains, so a save can never clobber the boards. A badge confirms the save, and open dashboard tabs sync the new bookmark live via a storage-change listener.
- **Free layout (bonus)** — Boards can be dragged to free positions via a drag handle, persisted per board. Positions are clamped back into view when the window shrinks, and the layout falls back to a stacked column on small screens. Each board can be pinned with a lock button: a locked board cannot be moved (its drag handle is hidden), preventing accidental repositioning. A drag only counts past a small movement threshold, so a mere click on the handle never shifts a board.
### Changed
- The bookmark- and board-delete paths no longer remove entries immediately; deletions now route through the trash.
- Chrome and Firefox manifests gain a background worker, an `action` entry and the `activeTab` / `commands` permissions to support Quick Save. Opera keeps its existing `tabs` permission and redirect worker.
---
## [2.2.0] — 2026-06-13
### Added
+22 -8
View File
@@ -1,6 +1,6 @@
# ⬡ Hellion Dashboard v2.0.0
# ⬡ Hellion Dashboard v2.4.0
![Version](https://img.shields.io/badge/Version-2.0.0-blue)
![Version](https://img.shields.io/badge/Version-2.4.0-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)
@@ -10,7 +10,7 @@
**No account. No subscription. No cloud. All data stays 100% local.**
A personal bookmark dashboard as a browser extension.
Boards, drag & drop, 11 themes, search bar, widget system with notes, calculator, timer and more.
Boards, drag & drop, free layout, command palette, trash, quick save, 11 themes plus a custom theme builder, search bar, widget system with notes, calculator, timer and more.
Full DE/EN language support with runtime switching. All in the browser, all offline.
No external data transmission, no trackers, no analytics, no ads.
@@ -38,8 +38,10 @@ What you see is what's saved. No magic.
### Boards & Bookmarks
- Boards as groups for links, sortable via drag & drop
- Free layout: drag boards to any position via a handle, each position is saved; a lock button pins a board in place
- Bookmarks with favicon, title and optional description
- Hide boards with the blur button (privacy mode)
- Trash: deleted bookmarks and boards are kept for 30 days before removal, with restore from Settings
- HTML import from browser bookmarks (Chrome, Edge, Firefox)
- JSON export & import (backup & restore)
@@ -48,6 +50,16 @@ What you see is what's saved. No magic.
- Google, DuckDuckGo or Bing, switchable with a click
- Toggleable via Settings
### Command Palette
- Open with **Ctrl+K**, live-filters all bookmarks (title and URL) and board names from the keyboard
- Arrow keys to navigate, Enter opens the match, Escape closes (read-only, separate from the web search bar)
### Quick Save
- Global shortcut (default **Alt+Shift+S**) saves the current tab into a fixed Inbox board from any page, without opening the dashboard
- A badge confirms the save; an open dashboard tab shows the new bookmark live
### Widget System
- **Notes & Checklists** — Floating note widgets with text or checklist template (max. 5)
@@ -58,7 +70,7 @@ What you see is what's saved. No magic.
- **Widget Toolbar** — Floating buttons for quick access, position (left/right) configurable in Settings
- All widgets: draggable, resizable, z-index stacking on click
### 11 Themes
### Themes
| Theme | Accent | Style |
|---|---|---|
@@ -74,6 +86,8 @@ What you see is what's saved. No magic.
| Avorion | `#2ec4a0` Turquoise | Deep Void |
| Hellion Stealth | `#5ec2ff` Tech Blue | Tactical Recon |
Plus a **custom theme**: build your own via the theme picker with six colour pickers (accent, background, board surface and three text levels). Colours apply live, the accent drives the derived glow, border and toggle tints, and a non-blocking WCAG contrast hint flags hard-to-read combinations without blocking the choice. Combinable with your own background image (local upload or https URL).
### Image Credits
| Theme | Source | License |
@@ -104,7 +118,7 @@ What you see is what's saved. No magic.
### Appearance & Settings
- **Appearance modal** (header button), theme picker, background image and all display options in one modal
- **Appearance modal** (header button), theme picker with custom theme builder, background image (local upload or https URL) and all display options in one modal
- **Settings panel** (header button), widgets, data & help, danger zone
- **About footer**, developer info, license and support links permanently visible
- Compact mode, shorten titles, search bar toggle, open links in new tab, descriptions, hide extra bookmarks
@@ -288,7 +302,7 @@ hellion-newtab/
- **Responsive** — Tablet (768px) and smartphone (480px) breakpoints
- **Secure** — `createElement` instead of `innerHTML`, URL validation, storage error handling
- **Event Delegation** — One listener per board list instead of per bookmark (performance)
- **Theme System** — CSS Custom Properties, 11 themes, custom background support
- **Theme System** — CSS Custom Properties, 11 themes plus a custom theme builder, custom background support (local upload or https URL)
---
@@ -317,8 +331,8 @@ hellion-newtab/
```bash
# Create a release:
git tag v2.0.0
git push origin v2.0.0
git tag v2.4.0
git push origin v2.4.0
# → GitHub Action automatically creates release with ZIP files
```
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2.2.0",
"version": "2.4.0",
"description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de",
@@ -54,7 +54,7 @@
}
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' https: data: blob:"
},
"icons": {
"16": "assets/icons/icon16.png",
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2.2.0",
"version": "2.4.0",
"description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de",
@@ -36,7 +36,7 @@
}
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' https: data: blob:"
},
"icons": {
"16": "assets/icons/icon16.png",
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2.2.0",
"version": "2.4.0",
"description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de",
@@ -51,7 +51,7 @@
},
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' https: data: blob:"
},
"icons": {
"16": "assets/icons/icon16.png",
+20 -2
View File
@@ -241,7 +241,7 @@
<div class="panel-footer">
<div class="about-block">
<div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div>
<div class="about-version">Version 2.2.0 · by Hellion Online Media</div>
<div class="about-version">Version 2.4.0 · by Hellion Online Media</div>
<div class="about-links">
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
@@ -370,6 +370,23 @@
<span class="theme-card-label">Stealth</span>
<span class="theme-card-check"></span>
</div>
<div class="theme-card theme-card-custom" data-value="custom" role="button" tabindex="0" aria-pressed="false" data-i18n-aria-label="theme.card.custom">
<span class="theme-card-custom-swatch"></span>
<span class="theme-card-label" data-i18n="theme.builder.title">Eigenes</span>
<span class="theme-card-check"></span>
</div>
</div>
<div class="theme-builder-panel hidden" id="themeBuilderPanel">
<div class="tb-grid">
<div class="tb-picker"><input type="color" id="tbAccent" value="#6c8cff"><label for="tbAccent" data-i18n="theme.builder.accent">Akzent</label></div>
<div class="tb-picker"><input type="color" id="tbBg" value="#0b0d12"><label for="tbBg" data-i18n="theme.builder.bg">Hintergrund</label></div>
<div class="tb-picker"><input type="color" id="tbBoard" value="#141821"><label for="tbBoard" data-i18n="theme.builder.board">Board-Fläche</label></div>
<div class="tb-picker"><input type="color" id="tbText" value="#e6e8ef"><label for="tbText" data-i18n="theme.builder.text">Text primär</label></div>
<div class="tb-picker"><input type="color" id="tbTextSec" value="#9aa3b8"><label for="tbTextSec" data-i18n="theme.builder.text_secondary">Text sekundär</label></div>
<div class="tb-picker"><input type="color" id="tbTextMuted" value="#5b6478"><label for="tbTextMuted" data-i18n="theme.builder.text_muted">Text gedämpft</label></div>
</div>
<div class="tb-contrast good" id="tbContrast"><span class="tb-dot"></span><span id="tbContrastText" data-i18n="theme.builder.contrast_good">Gut lesbar</span></div>
<div class="tb-foot"><button class="tb-reset" id="tbReset" data-i18n="theme.builder.reset">Zurücksetzen</button></div>
</div>
<div class="theme-modal-section">
<h3 class="settings-section-title" data-i18n="settings.section.bg">HINTERGRUND</h3>
@@ -381,9 +398,10 @@
<button class="btn-small" id="btnChangeBg" data-i18n="settings.bg_change">Ändern</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" />
<input type="text" class="text-input full-width" id="bgUrlInput" data-i18n-placeholder="settings.bg_url.placeholder" placeholder="https:// oder leer für Standard" />
<button class="btn-small" id="btnApplyBg" data-i18n="settings.bg_apply">Übernehmen</button>
</div>
<p class="setting-desc bg-url-hint hidden" id="bgUrlHint" data-i18n="settings.bg_url.privacy_hint">Hinweis: Ein per URL eingebundenes Bild wird bei jedem Öffnen vom fremden Server geladen.</p>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label" data-i18n="settings.bg_upload">Datei hochladen</span>
+64
View File
@@ -349,6 +349,30 @@
--toggle-on-bg: color-mix(in srgb, var(--accent) var(--toggle-on-bg-pct), transparent);
--bg-solid-fallback: #0d0f12;
}
/* ============================================
THEME: CUSTOM (User-Theme-Builder, neutrale Defaults)
Inline-Vars aus applyCustomTheme() ueberschreiben die 6 Picker-Werte.
============================================ */
[data-theme="custom"] {
--accent: #6c8cff;
--accent-glow-pct: 5%;
--board-hover-border-pct: 20%;
--logo-shadow-pct: 38%;
--toggle-on-bg-pct: 20%;
--bg-primary: #0b0d12;
--bg-board: rgba(20, 24, 33, 0.55);
--border: rgba(255, 255, 255, 0.06);
--text-primary: #e6e8ef;
--text-secondary: #9aa3b8;
--text-muted: #5b6478;
--font-display: 'Rajdhani', sans-serif;
--font-body: 'Inter', sans-serif;
--overlay-bg: radial-gradient(circle at center, color-mix(in srgb, var(--bg-primary) 35%, transparent) 0%, color-mix(in srgb, var(--bg-primary) 88%, transparent) 100%);
--header-bg: color-mix(in srgb, var(--bg-primary) 92%, transparent);
--toggle-on-bg: color-mix(in srgb, var(--accent) var(--toggle-on-bg-pct), transparent);
--bg-solid-fallback: var(--bg-primary);
}
}
/* ============================================
@@ -413,6 +437,8 @@
[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); }
[data-theme="custom"] .board { border-color: color-mix(in srgb, var(--accent) 15%, transparent); }
[data-theme="custom"] .bm-item:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
}
/* ============================================
@@ -568,9 +594,18 @@ html, body {
flex-shrink: 0;
transition: color 0.15s;
touch-action: none;
/* Ueber dem Blur-Overlay (.board-blur-overlay, z-index:5) liegen, sonst schluckt das
Overlay den pointerdown und ein geblurrtes Board ist nicht verschiebbar. Der Rest des
Boards bleibt unter dem Overlay -> Klick dort enthuellt weiterhin (Phase-5-Review). */
position: relative;
z-index: 6;
}
.board-drag-handle:hover { color: var(--accent); }
.board-drag-handle:active { cursor: grabbing; }
/* Gesperrtes Board (Position fixiert): Drag-Handle ausblenden (kein Verschieben mehr),
Lock-Button aktiv einfaerben. Hoehere Spezifitaet als .board-drag-handle -> display:none gewinnt. */
.board.locked .board-drag-handle { display: none; }
.board.locked .btn-lock-board { color: var(--accent); }
.board-title {
font-family: var(--font-display);
@@ -2378,6 +2413,35 @@ body.show-desc .bm-desc { display: block; }
.theme-modal-section .setting-row {
padding: 8px 0;
}
.bg-url-hint {
padding: 2px 0 6px;
line-height: 1.4;
}
.theme-card-custom-swatch {
width: 100%; height: 56px; border-radius: 8px;
border: 1.5px dashed var(--accent);
background: repeating-linear-gradient(45deg, rgba(255,255,255,0.03) 0 6px, rgba(255,255,255,0.06) 6px 12px);
}
.theme-builder-panel { padding: 10px 0 4px; }
.theme-builder-panel.hidden { display: none; }
.tb-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 18px; }
.tb-picker { display: flex; align-items: center; gap: 9px; }
.tb-picker input[type="color"] {
width: 30px; height: 30px; padding: 0; border: 1px solid var(--border);
border-radius: 6px; background: none; cursor: pointer;
}
.tb-picker label { font-size: 12px; color: var(--text-secondary); }
.tb-contrast { margin-top: 12px; font-size: 12px; display: flex; align-items: center; gap: 8px; }
.tb-contrast .tb-dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.tb-contrast.good { color: #7bd88f; } .tb-contrast.good .tb-dot { background: #3fbf6f; }
.tb-contrast.ok { color: #e3c97a; } .tb-contrast.ok .tb-dot { background: #d8b24a; }
.tb-contrast.bad { color: #e58f8f; } .tb-contrast.bad .tb-dot { background: #d65c5c; }
.tb-foot { display: flex; justify-content: flex-end; margin-top: 10px; }
.tb-reset {
font-size: 11px; color: var(--text-secondary); background: none;
border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; cursor: pointer;
}
@media (max-width: 480px) { .tb-grid { grid-template-columns: 1fr; } }
/* ============================================
ACCORDION SETTINGS
+31 -5
View File
@@ -119,7 +119,7 @@ async function checkBackupReminder() {
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : [];
const data = { version: '2.2.0', exported: new Date().toISOString(), boards, settings, trash, notes: notesData, calculator: calcHistory, timerPresets };
const data = { version: '2.4.0', exported: new Date().toISOString(), boards, settings, trash, 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');
@@ -241,6 +241,7 @@ function bindGlobalEvents() {
// gegenseitig ueberschreiben (Datensicherheit, Phase-4-Review-Blocker 2b).
let _drainBusy = false;
let _drainQueued = false; // ein waehrend eines laufenden Drains angefragter Drain wird nachgeholt
let _renderDeferredByDrag = false; // Drain hat den Render wegen eines laufenden Drags ausgelassen -> nach Drag-Ende nachholen
async function drainQuickSavePending() {
if (_drainBusy) { _drainQueued = true; return; }
_drainBusy = true;
@@ -250,10 +251,19 @@ async function drainQuickSavePending() {
const drained = pending.slice();
const drainedIds = new Set(drained.map(e => e && e.id).filter(Boolean));
const inbox = await ensureInboxBoard(); // legt die Inbox an, falls noetig; gibt das Board zurueck
// Idempotenz gegen den Worker/Drain-Race auf 'quicksave_pending': jede eingespielte Inbox-
// Bookmark traegt die Pending-id ihres Ursprungs als srcId. Taucht ein bereits gedrainter
// Eintrag durch einen gleichzeitigen Worker-Append erneut in der Queue auf, wird er hier
// uebersprungen statt doppelt eingefuegt — kein Duplikat, und kein Verlust (boards-Write
// bleibt vor der Queue-Bereinigung, daher keine umgekehrte Verlustgefahr).
const seenSrc = new Set(inbox.bookmarks.map(b => b && b.srcId).filter(Boolean));
for (const e of drained) {
if (e && typeof e.url === 'string' && e.url) {
inbox.bookmarks.push(normalizeBookmark({ title: e.title, url: e.url }));
}
if (!e || !e.id || seenSrc.has(e.id)) continue; // schon eingespielt
if (typeof e.url !== 'string' || !e.url || !isSafeUrl(e.url)) continue; // leeres/unsicheres Protokoll verwerfen
const bm = normalizeBookmark({ title: e.title, url: e.url });
bm.srcId = e.id; // Herkunft fuer kuenftige Dedup
inbox.bookmarks.push(bm);
seenSrc.add(e.id);
}
await saveBoards();
// NUR die verarbeiteten Eintraege entfernen — ein gleichzeitiger Worker-Append bleibt erhalten.
@@ -261,7 +271,14 @@ async function drainQuickSavePending() {
const remaining = Array.isArray(still) ? still.filter(e => e && !drainedIds.has(e.id)) : [];
await Store.set('quicksave_pending', remaining);
// Render nur, wenn gerade KEIN Drag laeuft (renderBoards->replaceChildren wuerde ihn abreissen).
if (!document.querySelector('.board.dragging, .bm-item.dragging-source')) renderBoards();
// Laeuft einer, den Render-Wunsch merken und nach Drag-Ende nachholen (drag.js ruft
// flushQuickSaveRenderIfDeferred), sonst bliebe der frisch gedrainte Quick-Save bis zu einem
// unabhaengigen Fremd-Render unsichtbar (Phase-6-Review).
if (document.querySelector('.board.dragging, .bm-item.dragging-source')) {
_renderDeferredByDrag = true;
} else {
renderBoards();
}
}
} catch (e) {
console.error('Quick-Save-Drain fehlgeschlagen:', e && e.message);
@@ -273,6 +290,15 @@ async function drainQuickSavePending() {
if (_drainQueued) { _drainQueued = false; drainQuickSavePending(); }
}
// Wird von drag.js nach jedem Drag-Ende aufgerufen: einen waehrend des Drags ausgelassenen
// Quick-Save-Render nachholen. Idempotent — tut nichts, wenn kein Render aussteht.
function flushQuickSaveRenderIfDeferred() {
if (_renderDeferredByDrag) {
_renderDeferredByDrag = false;
renderBoards();
}
}
// Live-Sync (QS-03): ein offener NewTab drained die Queue, sobald der Worker etwas anhaengt.
function bindStorageSync() {
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.onChanged) return;
+25 -3
View File
@@ -46,6 +46,13 @@ function createPlusSvg() {
]);
}
/** Erzeugt das Pin-/Reisszwecke-Icon SVG (Position fixieren) — bewusst KEIN Emoji (custom SVG). */
function createPinSvg() {
return svgEl('svg', { width: '11', height: '12', viewBox: '0 0 24 24', fill: 'currentColor' }, [
svgEl('path', { d: 'M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z' }),
]);
}
// ---- POS-MIGRATION ----
// Boards ohne pos (Altbestand vor v2.3) aus einem Auto-Raster befuellen,
// damit sie sich nicht alle auf (0,0) stapeln. Raster orientiert sich am
@@ -113,7 +120,7 @@ function renderBoards() {
function createBoardEl(board) {
const div = document.createElement('div');
div.className = 'board' + (board.blurred ? ' blurred' : '');
div.className = 'board' + (board.blurred ? ' blurred' : '') + (board.locked ? ' locked' : '');
div.dataset.boardId = board.id;
// Header
@@ -133,6 +140,11 @@ function createBoardEl(board) {
const actions = document.createElement('div');
actions.className = 'board-actions';
const btnLock = document.createElement('button');
btnLock.className = 'board-action-btn btn-lock-board';
btnLock.title = board.locked ? t('boards.unlock') : t('boards.lock');
btnLock.appendChild(createPinSvg());
const btnBlur = document.createElement('button');
btnBlur.className = 'board-action-btn btn-blur-board';
btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
@@ -152,9 +164,9 @@ function createBoardEl(board) {
}
if (btnDelete) {
actions.append(btnBlur, btnRename, btnDelete);
actions.append(btnLock, btnBlur, btnRename, btnDelete);
} else {
actions.append(btnBlur, btnRename);
actions.append(btnLock, btnBlur, btnRename);
}
header.append(dragHandle, titleSpanHeader, actions);
@@ -163,6 +175,16 @@ function createBoardEl(board) {
blurOverlay.className = 'board-blur-overlay';
div.appendChild(blurOverlay);
btnLock.addEventListener('click', async e => {
e.stopPropagation();
// Position fixieren: blendet via .board.locked den Drag-Handle aus (CSS) und der onDown-Guard
// in drag.js verweigert zusaetzlich den Drag. Reiner Klassen-Toggle, kein Re-Render noetig.
board.locked = !board.locked;
div.classList.toggle('locked', board.locked);
btnLock.title = board.locked ? t('boards.unlock') : t('boards.lock');
await saveBoards();
});
btnBlur.addEventListener('click', async e => {
e.stopPropagation();
board.blurred = !board.blurred;
+15 -7
View File
@@ -39,7 +39,7 @@ function initDataButtons() {
btnExport.addEventListener('click', async () => {
const widgetData = await Store.get('widgetStates');
const data = {
version: '2.2.0',
version: '2.4.0',
exported: new Date().toISOString(),
boards,
settings,
@@ -72,6 +72,7 @@ function initDataButtons() {
id: b.id || uid(),
title: String(b.title).slice(0, 100),
blurred: !!b.blurred,
locked: !!b.locked,
bookmarks: b.bookmarks
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
.map(bm => ({
@@ -109,6 +110,7 @@ function initDataButtons() {
id: e.item.id || uid(),
title: String(e.item.title || '').slice(0, 100),
blurred: !!e.item.blurred,
locked: !!e.item.locked,
// Position erhalten, damit ein wiederhergestelltes Board an seinem Platz landet.
...(safePos(e.item.pos) ? { pos: safePos(e.item.pos) } : {}),
bookmarks: Array.isArray(e.item.bookmarks)
@@ -124,12 +126,18 @@ function initDataButtons() {
}))
.filter(e => e.item !== null);
if (validTrash.length > 0) {
// Nach deletedAt aufsteigend sortieren, DANN die neuesten TRASH_MAX_ENTRIES behalten.
// Positionsbasiertes slice(-N) wuerde sonst frische lokale Eintraege verdraengen
// statt der aeltesten — Datenverlust, da ein Trash-Eintrag die einzige Kopie ist.
const combined = [...trash, ...validTrash];
combined.sort((a, b) => a.deletedAt - b.deletedAt);
trash = combined.slice(-TRASH_MAX_ENTRIES);
// Lokale Eintraege sind die EINZIGE Kopie ihrer geloeschten Daten -> Vorrang. Importierte
// stammen aus einem Backup, das der Nutzer noch besitzt -> nachrangig. Daher: erst ALLE
// lokalen behalten (pushToTrash kappt sie bereits auf TRASH_MAX_ENTRIES), dann mit den
// NEUESTEN importierten bis zur Obergrenze auffuellen. Ein frischer Import verdraengt so
// keine aelteren lokalen Sole-Copies mehr (frueheres sort+slice(-N) konnte das, data-loss).
const room = Math.max(0, TRASH_MAX_ENTRIES - trash.length);
const keptImports = validTrash
.slice()
.sort((a, b) => b.deletedAt - a.deletedAt) // neueste Importe zuerst
.slice(0, room);
// Am Ende nach deletedAt aufsteigend fuer eine stabile Anzeige-Reihenfolge.
trash = [...trash, ...keptImports].sort((a, b) => a.deletedAt - b.deletedAt);
await saveTrash();
}
}
+30 -9
View File
@@ -21,6 +21,9 @@ function initBoardDragDrop() {
handle.addEventListener('pointerdown', function onDown(e) {
// Auf Mobil ist .board position:static (Stapel) -> kein Free-Move.
if (getComputedStyle(boardEl).position !== 'absolute') return;
// Gesperrtes Board (Position fixiert, LAYOUT-LOCK) nicht verschieben. Der Drag-Handle ist
// bei .locked schon per CSS ausgeblendet; dieser Guard ist die zweite Sicherung.
if (boardEl.classList.contains('locked')) return;
e.preventDefault();
handle.setPointerCapture(e.pointerId);
// .board.dragging hebt das Board per CSS nach vorne (z-index) UND ist das Signal fuer den
@@ -31,8 +34,16 @@ function initBoardDragDrop() {
const rect = boardEl.getBoundingClientRect();
const offX = e.clientX - rect.left;
const offY = e.clientY - rect.top;
const startCX = e.clientX, startCY = e.clientY;
// Erst eine echte Bewegung (> 3px) zaehlt als Drag. Ein reiner Klick/Tap auf den Handle darf
// board.pos NICHT ueberschreiben: renderBoards() schreibt in --board-x/y den gegen die Viewport
// GECLAMPTEN Wert, board.pos bleibt absichtlich der wahre (evtl. off-screen) Wert. onUp liest
// --board-x/y zurueck — bei einem No-Move-Klick waere das der Clamp und wuerde die wahre
// Position zerstoeren (Phase-5-Review, HIGH/data-loss).
let moved = false;
function onMove(ev) {
if (Math.abs(ev.clientX - startCX) > 3 || Math.abs(ev.clientY - startCY) > 3) moved = true;
const maxX = window.innerWidth - boardEl.offsetWidth;
const maxY = window.innerHeight - boardEl.offsetHeight;
const x = Math.max(0, Math.min(maxX, ev.clientX - offX));
@@ -54,20 +65,28 @@ function initBoardDragDrop() {
async function onUp() {
cleanup();
const id = boardEl.dataset.boardId;
const board = boards.find(b => b.id === id);
if (board) {
board.pos = {
x: parseFloat(boardEl.style.getPropertyValue('--board-x')),
y: parseFloat(boardEl.style.getPropertyValue('--board-y'))
};
await saveBoards();
// Nur bei echtem Verschieben persistieren — sonst board.pos unangetastet lassen.
if (moved) {
const id = boardEl.dataset.boardId;
const board = boards.find(b => b.id === id);
if (board) {
board.pos = {
x: parseFloat(boardEl.style.getPropertyValue('--board-x')),
y: parseFloat(boardEl.style.getPropertyValue('--board-y'))
};
await saveBoards();
}
}
// Einen waehrend des Drags ausgelassenen Quick-Save-Render nachholen (app.js).
if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred();
}
// pointercancel feuert STATT pointerup bei Touch-Interrupt, Browser-Geste oder wenn das
// captured Element aus dem DOM faellt -> nur aufraeumen (cleanup), pos NICHT ueberschreiben.
function onCancel() { cleanup(); }
function onCancel() {
cleanup();
if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred();
}
handle.addEventListener('pointermove', onMove);
handle.addEventListener('pointerup', onUp);
@@ -91,6 +110,8 @@ function initBookmarkDragDrop(listEl, board) {
listEl.addEventListener('dragend', e => {
const item = e.target.closest('.bm-item');
if (item) item.classList.remove('dragging-source');
// Blieb ein Quick-Save-Render waehrend des Bookmark-Drags aus (Drop ausgeblieben), jetzt nachholen.
if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred();
});
listEl.addEventListener('dragover', e => {
+40 -8
View File
@@ -21,6 +21,8 @@ const STRINGS = {
'boards.drag_title': 'Board verschieben',
'boards.blur': 'Blur (privat)',
'boards.unblur': 'Unblur',
'boards.lock': 'Position sperren',
'boards.unlock': 'Position entsperren',
'boards.rename': 'Umbenennen',
'boards.delete': 'Löschen',
'boards.delete_confirm': 'Board „{title}" wirklich löschen?',
@@ -64,8 +66,8 @@ const STRINGS = {
'onboarding.s2.f2': 'Importiere Browser-Lesezeichen über den „Import" Button im Header',
'onboarding.s2.f3': 'Drag & Drop zum Umsortieren von Boards und Links',
'onboarding.s2.f4': 'Blur-Modus für private Boards (🔒 Icon)',
'onboarding.s3.title': '11 handgefertigte Themes',
'onboarding.s3.text': 'Klicke auf den „Theme" Button im Header um dein Theme zu wählen. Jedes hat seinen eigenen Stil und Farbpalette.',
'onboarding.s3.title': 'Handgefertigte Themes + dein eigenes',
'onboarding.s3.text': 'Klicke auf den „Theme" Button im Header, um ein Theme zu wählen oder dir ein eigenes zu bauen. Jedes hat seinen eigenen Stil.',
'onboarding.s4.title': 'Widget-Toolbar',
'onboarding.s4.f1': 'Die schwebenden Buttons rechts öffnen Widgets',
'onboarding.s4.f2': 'Notes und Checklisten für schnelle Notizen',
@@ -383,7 +385,9 @@ const STRINGS = {
'settings.visible_count': 'Sichtbare Bookmarks',
'settings.visible_count.desc': 'Anzahl vor dem Ausblenden',
'settings.bg_url': 'Bild-URL',
'settings.bg_url.desc': 'Eigenes Hintergrundbild per URL',
'settings.bg_url.desc': 'Eigenes Bild per https-URL oder lokalem Upload',
'settings.bg_url.placeholder': 'https://… oder leer für Standard',
'settings.bg_url.privacy_hint': 'Hinweis: Ein per URL eingebundenes Bild wird bei jedem Öffnen vom fremden Server geladen.',
'settings.bg_change': 'Ändern',
'settings.bg_apply': 'Übernehmen',
'settings.bg_upload': 'Datei hochladen',
@@ -394,7 +398,7 @@ const STRINGS = {
'settings.onboarding_btn': 'Start',
'settings.reset_btn': 'Reset',
'settings.bg_upload_btn': 'Upload',
'settings.bg_invalid_url': 'Nur lokale Bilder (Upload) sind als Hintergrund erlaubt.',
'settings.bg_invalid_url': 'Nur https-URLs oder lokale Bilder (Upload) sind als Hintergrund erlaubt.',
'settings.bg_invalid_url.title': 'Ungültige URL',
// Modals
@@ -421,6 +425,18 @@ const STRINGS = {
'theme.card.satisfactory': 'Theme Satisfactory wählen',
'theme.card.avorion': 'Theme Avorion wählen',
'theme.card.hellion_stealth': 'Theme Hellion Stealth wählen',
'theme.card.custom': 'Eigenes Theme wählen',
'theme.builder.title': 'Eigenes',
'theme.builder.accent': 'Akzent',
'theme.builder.bg': 'Hintergrund',
'theme.builder.board': 'Board-Fläche',
'theme.builder.text': 'Text primär',
'theme.builder.text_secondary': 'Text sekundär',
'theme.builder.text_muted': 'Text gedämpft',
'theme.builder.reset': 'Zurücksetzen',
'theme.builder.contrast_good': 'Gut lesbar',
'theme.builder.contrast_ok': 'Grenzwertig',
'theme.builder.contrast_bad': 'Schwer lesbar',
'toolbar.label': 'Widget-Werkzeugleiste',
// About
@@ -480,6 +496,8 @@ const STRINGS = {
'boards.drag_title': 'Move board',
'boards.blur': 'Blur (private)',
'boards.unblur': 'Unblur',
'boards.lock': 'Lock position',
'boards.unlock': 'Unlock position',
'boards.rename': 'Rename',
'boards.delete': 'Delete',
'boards.delete_confirm': 'Really delete board "{title}"?',
@@ -523,8 +541,8 @@ const STRINGS = {
'onboarding.s2.f2': 'Import browser bookmarks via the "Import" button in the header',
'onboarding.s2.f3': 'Drag & drop to reorder boards and links',
'onboarding.s2.f4': 'Blur mode for private boards (🔒 icon)',
'onboarding.s3.title': '11 handcrafted themes',
'onboarding.s3.text': 'Click the "Theme" button in the header to choose your theme. Each has its own style and color palette.',
'onboarding.s3.title': 'Hand-crafted themes + your own',
'onboarding.s3.text': 'Click the "Theme" button in the header to choose a theme or build your own. Each has its own style.',
'onboarding.s4.title': 'Widget Toolbar',
'onboarding.s4.f1': 'The floating buttons on the right open widgets',
'onboarding.s4.f2': 'Notes and checklists for quick notes',
@@ -842,7 +860,9 @@ const STRINGS = {
'settings.visible_count': 'Visible bookmarks',
'settings.visible_count.desc': 'Number before hiding',
'settings.bg_url': 'Image URL',
'settings.bg_url.desc': 'Custom background image via URL',
'settings.bg_url.desc': 'Custom image via https URL or local upload',
'settings.bg_url.placeholder': 'https://… or empty for default',
'settings.bg_url.privacy_hint': 'Note: an image loaded via URL is fetched from the remote server every time you open a tab.',
'settings.bg_change': 'Change',
'settings.bg_apply': 'Apply',
'settings.bg_upload': 'Upload file',
@@ -853,7 +873,7 @@ const STRINGS = {
'settings.onboarding_btn': 'Start',
'settings.reset_btn': 'Reset',
'settings.bg_upload_btn': 'Upload',
'settings.bg_invalid_url': 'Only local images (upload) are allowed as background.',
'settings.bg_invalid_url': 'Only https URLs or local images (upload) are allowed as background.',
'settings.bg_invalid_url.title': 'Invalid URL',
// Modals
@@ -880,6 +900,18 @@ const STRINGS = {
'theme.card.satisfactory': 'Select Satisfactory theme',
'theme.card.avorion': 'Select Avorion theme',
'theme.card.hellion_stealth': 'Select Hellion Stealth theme',
'theme.card.custom': 'Select custom theme',
'theme.builder.title': 'Custom',
'theme.builder.accent': 'Accent',
'theme.builder.bg': 'Background',
'theme.builder.board': 'Board surface',
'theme.builder.text': 'Text primary',
'theme.builder.text_secondary': 'Text secondary',
'theme.builder.text_muted': 'Text muted',
'theme.builder.reset': 'Reset',
'theme.builder.contrast_good': 'Good contrast',
'theme.builder.contrast_ok': 'Borderline',
'theme.builder.contrast_bad': 'Hard to read',
'toolbar.label': 'Widget toolbar',
// About
+12
View File
@@ -34,6 +34,17 @@
return inbox;
}
// Sicheres URL-Protokoll (http/https/ftp). Inhaltlich identisch zur data.js-Variante, aber
// DOM-frei und auf globalThis, damit der Quick-Save-Drain (app.js) dieselbe Validierung nutzt
// wie jeder andere Bookmark-Schreibpfad. URL ist in Worker UND Seite verfuegbar.
function isSafeUrl(url) {
try {
return ['http:', 'https:', 'ftp:'].includes(new URL(url).protocol);
} catch (_) {
return false;
}
}
// Normalisiert eine Bookmark in die kanonische Form { id, title, url, desc }.
// title-Fallback auf url, desc auf ''. Begrenzt Laengen wie data.js (200/500),
// damit Quick-Save-Eintraege das gleiche Schema wie Import/Manuell haben.
@@ -52,6 +63,7 @@
root.INBOX_ID = INBOX_ID;
root.uid = uid;
root.isSafeUrl = isSafeUrl;
root.ensureInbox = ensureInbox;
root.normalizeBookmark = normalizeBookmark;
})(typeof globalThis !== 'undefined' ? globalThis : self);
+186 -7
View File
@@ -80,6 +80,8 @@ function openThemeModal() {
document.addEventListener('keydown', _themeTrap);
const first = _focusable(modal)[0];
if (first) first.focus();
syncCustomPickers();
document.getElementById('themeBuilderPanel').classList.toggle('hidden', settings.theme !== 'custom');
}
function closeThemeModal() {
const overlay = document.getElementById('themeOverlay');
@@ -113,7 +115,131 @@ function switchTheme(name) {
*/
function isValidBgUrl(url) {
return typeof url === 'string' && url.length > 0 &&
(url.startsWith('blob:') || url.startsWith('data:image/'));
(url.startsWith('blob:') || url.startsWith('data:image/') || url.startsWith('https://'));
}
// ---- THEME-BUILDER: Konstanten + reine Helfer ----
const CUSTOM_DEFAULTS = {
accent: '#6c8cff', bgPrimary: '#0b0d12', bgBoard: '#141821',
textPrimary: '#e6e8ef', textSecondary: '#9aa3b8', textMuted: '#5b6478',
};
const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
function isValidHexColor(v) { return typeof v === 'string' && HEX_RE.test(v); }
function safeHex(v, fallback) { return isValidHexColor(v) ? v : fallback; }
function hexToRgba(hex, alpha) {
let h = hex.replace('#', '');
if (h.length === 3) h = h.split('').map(c => c + c).join('');
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
// WCAG 2.x Relativluminanz + Kontrastverhaeltnis
function relLuminance(hex) {
let h = hex.replace('#', '');
if (h.length === 3) h = h.split('').map(c => c + c).join('');
const lin = [0, 2, 4].map(i => {
const c = parseInt(h.slice(i, i + 2), 16) / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
}
function contrastRatio(hexA, hexB) {
const a = relLuminance(hexA), b = relLuminance(hexB);
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
}
function updateContrastIndicator(textHex, bgHex) {
const el = document.getElementById('tbContrast');
if (!el) return;
const ratio = contrastRatio(textHex, bgHex);
let cls, key;
if (ratio >= 4.5) { cls = 'good'; key = 'theme.builder.contrast_good'; }
else if (ratio >= 3) { cls = 'ok'; key = 'theme.builder.contrast_ok'; }
else { cls = 'bad'; key = 'theme.builder.contrast_bad'; }
el.classList.remove('good', 'ok', 'bad');
el.classList.add(cls);
const txt = document.getElementById('tbContrastText');
if (txt) txt.textContent = `${t(key)} (${ratio.toFixed(1)}:1)`;
}
// Setzt data-theme='custom' + 6 validierte Inline-Vars (Gate vor jedem setProperty).
function applyCustomTheme(ct) {
const root = document.documentElement;
const c = ct || {};
const accent = safeHex(c.accent, CUSTOM_DEFAULTS.accent);
const bgPrimary = safeHex(c.bgPrimary, CUSTOM_DEFAULTS.bgPrimary);
const bgBoard = safeHex(c.bgBoard, CUSTOM_DEFAULTS.bgBoard);
const textPrimary = safeHex(c.textPrimary, CUSTOM_DEFAULTS.textPrimary);
const textSecondary = safeHex(c.textSecondary, CUSTOM_DEFAULTS.textSecondary);
const textMuted = safeHex(c.textMuted, CUSTOM_DEFAULTS.textMuted);
root.setAttribute('data-theme', 'custom');
root.style.setProperty('--accent', accent);
root.style.setProperty('--bg-primary', bgPrimary);
root.style.setProperty('--bg-board', hexToRgba(bgBoard, 0.55));
root.style.setProperty('--text-primary', textPrimary);
root.style.setProperty('--text-secondary', textSecondary);
root.style.setProperty('--text-muted', textMuted);
document.querySelectorAll('.theme-card').forEach(card => {
const on = card.dataset.value === 'custom';
card.classList.toggle('active', on);
card.setAttribute('aria-pressed', on ? 'true' : 'false');
});
updateContrastIndicator(textPrimary, bgPrimary);
// Kein eigenes Bild gesetzt -> bgLayer leeren, damit --bg-primary (Solid) durchscheint
// statt des Hintergrundbilds eines zuvor gewaehlten Presets (das sonst haengen bliebe).
if (!(settings.bgUrl && isValidBgUrl(settings.bgUrl))) {
document.getElementById('bgLayer').style.backgroundImage = '';
}
}
// Entfernt die 6 Inline-Vars (Rueckwechsel auf Preset / Reset).
function clearCustomTheme() {
const root = document.documentElement;
['--accent', '--bg-primary', '--bg-board', '--text-primary', '--text-secondary', '--text-muted']
.forEach(v => root.style.removeProperty(v));
}
// Schreibt die gespeicherten (oder Default-) Farben in die 6 Picker-Inputs.
function syncCustomPickers() {
const ct = settings.customTheme || {};
const set = (id, key) => { const el = document.getElementById(id); if (el) el.value = safeHex(ct[key], CUSTOM_DEFAULTS[key]); };
set('tbAccent', 'accent'); set('tbBg', 'bgPrimary'); set('tbBoard', 'bgBoard');
set('tbText', 'textPrimary'); set('tbTextSec', 'textSecondary'); set('tbTextMuted', 'textMuted');
}
// Eigenes Upload-Bild Quota-schonend verkleinern: auf die laengste Bildschirmkante
// (× devicePixelRatio, gedeckelt) herunterrechnen und als WebP neu kodieren. Das spart
// gegenueber dem rohen Base64-Upload locker den Grossteil der chrome.storage.local-Quota.
// Greift nur beim lokalen Upload (data:-URL ist same-origin, Canvas wird nicht getainted);
// https-Hintergruende liegen remote und kosten keine Quota.
function downscaleBgImage(dataUrl) {
const MAX_DIM = Math.min(2560, Math.round(Math.max(screen.width, screen.height) * (window.devicePixelRatio || 1)));
const QUALITY = 0.82;
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const scale = Math.min(1, MAX_DIM / Math.max(img.naturalWidth, img.naturalHeight));
const w = Math.max(1, Math.round(img.naturalWidth * scale));
const h = Math.max(1, Math.round(img.naturalHeight * scale));
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
if (!ctx) { resolve(dataUrl); return; } // kein 2D-Context -> Original behalten
ctx.drawImage(img, 0, 0, w, h);
// WebP wo verfuegbar (Chrome/Opera/FF142+); sonst faellt toDataURL auf PNG zurueck -> dann JPEG
let out = canvas.toDataURL('image/webp', QUALITY);
if (!out.startsWith('data:image/webp')) out = canvas.toDataURL('image/jpeg', QUALITY);
resolve(out);
};
img.onerror = () => reject(new Error('image decode failed'));
img.src = dataUrl;
});
}
// ---- ACCORDION ----
@@ -344,7 +470,11 @@ function applySettings() {
const langEl = document.getElementById('settingLanguage');
if (langEl) langEl.value = settings.language || 'auto';
applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
if (settings.theme === 'custom') {
applyCustomTheme(settings.customTheme);
} else {
applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
}
if (settings.bgUrl && isValidBgUrl(settings.bgUrl)) {
document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`;
@@ -371,10 +501,25 @@ function bindSettingsEvents() {
const themeCards = document.querySelectorAll('.theme-card');
function selectThemeCard(card) {
const name = card.dataset.value;
if (!name || name === settings.theme) return Promise.resolve();
if (!name) return Promise.resolve();
// Custom: VOR dem name===settings.theme-Guard, damit ein Re-Klick das Panel wieder oeffnet.
if (name === 'custom') {
settings.theme = 'custom';
if (!settings.customTheme) settings.customTheme = { ...CUSTOM_DEFAULTS };
themeCards.forEach(c => c.setAttribute('aria-pressed', c === card ? 'true' : 'false'));
applyCustomTheme(settings.customTheme); // setzt data-theme + Inline-Vars; bgUrl UNANGETASTET (Koexistenz)
syncCustomPickers();
document.getElementById('themeBuilderPanel').classList.remove('hidden');
return saveSettings();
}
if (name === settings.theme) return Promise.resolve();
settings.theme = name;
settings.bgUrl = '';
document.getElementById('bgUrlInput').value = '';
clearCustomTheme(); // Inline-Vars weg beim Rueckwechsel auf ein Preset
document.getElementById('themeBuilderPanel').classList.add('hidden');
// aria-pressed synchron halten — applyTheme/switchTheme pflegt nur die .active-Klasse, nicht ARIA
themeCards.forEach(c => c.setAttribute('aria-pressed', c === card ? 'true' : 'false'));
switchTheme(name); // WICHTIG: switchTheme aus Phase 4 (View-Transition-Wrapper), NICHT applyTheme direkt — sonst geht der Theme-Fade verloren
@@ -390,6 +535,31 @@ function bindSettingsEvents() {
});
});
// Theme-Builder Picker
const TB_PICKERS = [['tbAccent', 'accent'], ['tbBg', 'bgPrimary'], ['tbBoard', 'bgBoard'],
['tbText', 'textPrimary'], ['tbTextSec', 'textSecondary'], ['tbTextMuted', 'textMuted']];
TB_PICKERS.forEach(([id, key]) => {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('input', () => { // live waehrend des Ziehens
if (!settings.customTheme) settings.customTheme = { ...CUSTOM_DEFAULTS };
settings.customTheme[key] = el.value;
settings.theme = 'custom';
applyCustomTheme(settings.customTheme);
});
el.addEventListener('change', () => saveSettings()); // persistiert beim Loslassen/Schliessen
});
const tbReset = document.getElementById('tbReset');
if (tbReset) {
tbReset.addEventListener('click', async () => {
settings.customTheme = { ...CUSTOM_DEFAULTS };
applyCustomTheme(settings.customTheme);
syncCustomPickers();
await saveSettings();
});
}
// Accordion initialisieren
initAccordion();
@@ -434,7 +604,9 @@ function bindSettingsEvents() {
// Background URL (im Theme-Modal)
document.getElementById('btnChangeBg').addEventListener('click', () => {
document.getElementById('bgInputRow').classList.toggle('hidden');
// toggle() liefert true, wenn 'hidden' jetzt gesetzt ist -> Hinweis exakt parallel schalten
const isNowHidden = document.getElementById('bgInputRow').classList.toggle('hidden');
document.getElementById('bgUrlHint').classList.toggle('hidden', isNowHidden);
});
document.getElementById('btnApplyBg').addEventListener('click', async () => {
const url = document.getElementById('bgUrlInput').value.trim();
@@ -458,8 +630,14 @@ function bindSettingsEvents() {
const reader = new FileReader();
reader.onload = async ev => {
if (!isValidBgUrl(ev.target.result)) return;
settings.bgUrl = ev.target.result;
document.getElementById('bgLayer').style.backgroundImage = `url('${ev.target.result}')`;
let bg = ev.target.result;
try {
bg = await downscaleBgImage(bg); // Quota-Schutz: verkleinern + WebP
} catch {
// Downscale fehlgeschlagen -> Original-Upload nutzen (besser als gar kein Bild)
}
settings.bgUrl = bg;
document.getElementById('bgLayer').style.backgroundImage = `url('${bg}')`;
await saveSettings();
};
reader.onerror = () => {
@@ -512,7 +690,8 @@ function bindSettingsEvents() {
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
showSearch: true, searchEngine: 'google', toolbarPos: 'right',
imageRefEnabled: false, language: 'auto' };
imageRefEnabled: false, language: 'auto', customTheme: null };
clearCustomTheme();
await saveBoards();
await saveTrash();
await saveSettings();
+2
View File
@@ -25,6 +25,7 @@ let settings = {
visibleCount: 10,
bgUrl: '',
theme: 'nebula',
customTheme: null,
showSearch: true,
searchEngine: 'google',
toolbarPos: 'right',
@@ -54,6 +55,7 @@ function getDefaultBoards() {
{ id: uid(), title: 'Next.js Docs', url: 'https://nextjs.org/docs', desc: '' },
],
blurred: false,
locked: false,
pos: { x: 40, y: 110 }
}
];