merge: v2.4.0 theme builder into development

This commit is contained in:
2026-06-15 08:11:12 +02:00
12 changed files with 360 additions and 47 deletions
+41 -22
View File
@@ -77,28 +77,47 @@ jobs:
sha256sum *.zip > checksums-sha256.txt sha256sum *.zip > checksums-sha256.txt
cat checksums-sha256.txt cat checksums-sha256.txt
# Gitea-native Release-Action. Legt das Release an, falls der Tag noch # Release per Gitea-API (curl), NICHT via gitea.com/actions/release-action: die ist `using: go`
# keins hat, oder aktualisiert das bestehende und haengt die Assets an. # und stirbt auf diesem Runner mit exit 127 ("go not found"), weil act_runner v0.6.1 die go-Action
# Der auto-injizierte GITHUB_TOKEN auf Gitea Actions hat Gitea-API-Scope # weder im Job-Image noch im Runner kompiliert bekommt. curl + python3 sind im Job-Image vorhanden
# und reicht fuer Release-Write. # und laufen als normaler Step -> unabhaengig von go-Toolchain, Action-Cache und @main-Drift.
- name: Attach to Gitea release # GITHUB_API_URL/GITHUB_REPOSITORY/GITHUB_TOKEN injiziert Gitea Actions automatisch.
uses: https://gitea.com/actions/release-action@main - name: Create release & upload assets (Gitea API)
with: env:
files: |- GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
dist/hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip TAG: ${{ steps.version.outputs.tag }}
dist/hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip run: |
dist/hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip set -euo pipefail
dist/checksums-sha256.txt API="${GITHUB_API_URL:-https://gitea.hellion-forge.cloud/api/v1}"
api_key: ${{ secrets.GITHUB_TOKEN }} REPO="${GITHUB_REPOSITORY}"
body: | AUTH="Authorization: token ${GITEA_TOKEN}"
## Hellion NewTab ${{ steps.version.outputs.tag }}
### Installation # Release-Request-JSON (Body inkl. Installationshinweise) als python-Einzeiler bauen
- **Chrome / Edge / Brave / Vivaldi:** `hellion-newtab-${{ steps.version.outputs.tag }}-chrome.zip` # (mehrzeilig wuerde den YAML-run-Block brechen: Zeilen auf Spalte 0).
- **Firefox:** `hellion-newtab-${{ steps.version.outputs.tag }}-firefox.zip` 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}))')
- **Opera / Opera GX:** `hellion-newtab-${{ steps.version.outputs.tag }}-opera.zip`
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 # Vorhandene gleichnamige Assets entfernen (idempotent bei Re-Runs), dann hochladen.
`checksums-sha256.txt` zum Verifizieren der Dateiintegritaet. 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."
+13
View File
@@ -6,6 +6,19 @@ 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 ## [2.3.0] — 2026-06-14
### Added ### Added
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"default_locale": "en", "default_locale": "en",
"version": "2.3.0", "version": "2.4.0",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling", "author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de", "homepage_url": "https://hellion-media.de",
@@ -54,7 +54,7 @@
} }
], ],
"content_security_policy": { "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": { "icons": {
"16": "assets/icons/icon16.png", "16": "assets/icons/icon16.png",
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"default_locale": "en", "default_locale": "en",
"version": "2.3.0", "version": "2.4.0",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling", "author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de", "homepage_url": "https://hellion-media.de",
@@ -36,7 +36,7 @@
} }
], ],
"content_security_policy": { "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": { "icons": {
"16": "assets/icons/icon16.png", "16": "assets/icons/icon16.png",
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"default_locale": "en", "default_locale": "en",
"version": "2.3.0", "version": "2.4.0",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"author": "Hellion Online Media - Florian Wathling", "author": "Hellion Online Media - Florian Wathling",
"homepage_url": "https://hellion-media.de", "homepage_url": "https://hellion-media.de",
@@ -51,7 +51,7 @@
}, },
"content_security_policy": { "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": { "icons": {
"16": "assets/icons/icon16.png", "16": "assets/icons/icon16.png",
+20 -2
View File
@@ -241,7 +241,7 @@
<div class="panel-footer"> <div class="panel-footer">
<div class="about-block"> <div class="about-block">
<div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div> <div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div>
<div class="about-version">Version 2.3.0 · by Hellion Online Media</div> <div class="about-version">Version 2.4.0 · by Hellion Online Media</div>
<div class="about-links"> <div class="about-links">
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link"> <a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
@@ -370,6 +370,23 @@
<span class="theme-card-label">Stealth</span> <span class="theme-card-label">Stealth</span>
<span class="theme-card-check"></span> <span class="theme-card-check"></span>
</div> </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>
<div class="theme-modal-section"> <div class="theme-modal-section">
<h3 class="settings-section-title" data-i18n="settings.section.bg">HINTERGRUND</h3> <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> <button class="btn-small" id="btnChangeBg" data-i18n="settings.bg_change">Ändern</button>
</div> </div>
<div class="setting-row hidden" id="bgInputRow"> <div class="setting-row hidden" id="bgInputRow">
<input type="text" class="text-input full-width" id="bgUrlInput" placeholder="https://... oder leer für Standard" /> <input type="text" class="text-input full-width" id="bgUrlInput" 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> <button class="btn-small" id="btnApplyBg" data-i18n="settings.bg_apply">Übernehmen</button>
</div> </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-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label" data-i18n="settings.bg_upload">Datei hochladen</span> <span class="setting-label" data-i18n="settings.bg_upload">Datei hochladen</span>
+55
View File
@@ -349,6 +349,30 @@
--toggle-on-bg: color-mix(in srgb, var(--accent) var(--toggle-on-bg-pct), transparent); --toggle-on-bg: color-mix(in srgb, var(--accent) var(--toggle-on-bg-pct), transparent);
--bg-solid-fallback: #0d0f12; --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-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"] .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="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); }
} }
/* ============================================ /* ============================================
@@ -2387,6 +2413,35 @@ body.show-desc .bm-desc { display: block; }
.theme-modal-section .setting-row { .theme-modal-section .setting-row {
padding: 8px 0; 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 ACCORDION SETTINGS
+1 -1
View File
@@ -119,7 +119,7 @@ async function checkBackupReminder() {
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : []; const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : []; const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : []; const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : [];
const data = { version: '2.3.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 blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
+1 -1
View File
@@ -39,7 +39,7 @@ function initDataButtons() {
btnExport.addEventListener('click', async () => { btnExport.addEventListener('click', async () => {
const widgetData = await Store.get('widgetStates'); const widgetData = await Store.get('widgetStates');
const data = { const data = {
version: '2.3.0', version: '2.4.0',
exported: new Date().toISOString(), exported: new Date().toISOString(),
boards, boards,
settings, settings,
+36 -8
View File
@@ -66,8 +66,8 @@ const STRINGS = {
'onboarding.s2.f2': 'Importiere Browser-Lesezeichen über den „Import" Button im Header', '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.f3': 'Drag & Drop zum Umsortieren von Boards und Links',
'onboarding.s2.f4': 'Blur-Modus für private Boards (🔒 Icon)', 'onboarding.s2.f4': 'Blur-Modus für private Boards (🔒 Icon)',
'onboarding.s3.title': '11 handgefertigte Themes', 'onboarding.s3.title': 'Handgefertigte Themes + dein eigenes',
'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.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.title': 'Widget-Toolbar',
'onboarding.s4.f1': 'Die schwebenden Buttons rechts öffnen Widgets', 'onboarding.s4.f1': 'Die schwebenden Buttons rechts öffnen Widgets',
'onboarding.s4.f2': 'Notes und Checklisten für schnelle Notizen', 'onboarding.s4.f2': 'Notes und Checklisten für schnelle Notizen',
@@ -385,7 +385,9 @@ const STRINGS = {
'settings.visible_count': 'Sichtbare Bookmarks', 'settings.visible_count': 'Sichtbare Bookmarks',
'settings.visible_count.desc': 'Anzahl vor dem Ausblenden', 'settings.visible_count.desc': 'Anzahl vor dem Ausblenden',
'settings.bg_url': 'Bild-URL', '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_change': 'Ändern',
'settings.bg_apply': 'Übernehmen', 'settings.bg_apply': 'Übernehmen',
'settings.bg_upload': 'Datei hochladen', 'settings.bg_upload': 'Datei hochladen',
@@ -396,7 +398,7 @@ const STRINGS = {
'settings.onboarding_btn': 'Start', 'settings.onboarding_btn': 'Start',
'settings.reset_btn': 'Reset', 'settings.reset_btn': 'Reset',
'settings.bg_upload_btn': 'Upload', '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', 'settings.bg_invalid_url.title': 'Ungültige URL',
// Modals // Modals
@@ -423,6 +425,18 @@ const STRINGS = {
'theme.card.satisfactory': 'Theme Satisfactory wählen', 'theme.card.satisfactory': 'Theme Satisfactory wählen',
'theme.card.avorion': 'Theme Avorion wählen', 'theme.card.avorion': 'Theme Avorion wählen',
'theme.card.hellion_stealth': 'Theme Hellion Stealth 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', 'toolbar.label': 'Widget-Werkzeugleiste',
// About // About
@@ -527,8 +541,8 @@ const STRINGS = {
'onboarding.s2.f2': 'Import browser bookmarks via the "Import" button in the header', '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.f3': 'Drag & drop to reorder boards and links',
'onboarding.s2.f4': 'Blur mode for private boards (🔒 icon)', 'onboarding.s2.f4': 'Blur mode for private boards (🔒 icon)',
'onboarding.s3.title': '11 handcrafted themes', 'onboarding.s3.title': 'Hand-crafted themes + your own',
'onboarding.s3.text': 'Click the "Theme" button in the header to choose your theme. Each has its own style and color palette.', '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.title': 'Widget Toolbar',
'onboarding.s4.f1': 'The floating buttons on the right open widgets', 'onboarding.s4.f1': 'The floating buttons on the right open widgets',
'onboarding.s4.f2': 'Notes and checklists for quick notes', 'onboarding.s4.f2': 'Notes and checklists for quick notes',
@@ -846,7 +860,9 @@ const STRINGS = {
'settings.visible_count': 'Visible bookmarks', 'settings.visible_count': 'Visible bookmarks',
'settings.visible_count.desc': 'Number before hiding', 'settings.visible_count.desc': 'Number before hiding',
'settings.bg_url': 'Image URL', '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_change': 'Change',
'settings.bg_apply': 'Apply', 'settings.bg_apply': 'Apply',
'settings.bg_upload': 'Upload file', 'settings.bg_upload': 'Upload file',
@@ -857,7 +873,7 @@ const STRINGS = {
'settings.onboarding_btn': 'Start', 'settings.onboarding_btn': 'Start',
'settings.reset_btn': 'Reset', 'settings.reset_btn': 'Reset',
'settings.bg_upload_btn': 'Upload', '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', 'settings.bg_invalid_url.title': 'Invalid URL',
// Modals // Modals
@@ -884,6 +900,18 @@ const STRINGS = {
'theme.card.satisfactory': 'Select Satisfactory theme', 'theme.card.satisfactory': 'Select Satisfactory theme',
'theme.card.avorion': 'Select Avorion theme', 'theme.card.avorion': 'Select Avorion theme',
'theme.card.hellion_stealth': 'Select Hellion Stealth 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', 'toolbar.label': 'Widget toolbar',
// About // About
+186 -7
View File
@@ -80,6 +80,8 @@ function openThemeModal() {
document.addEventListener('keydown', _themeTrap); document.addEventListener('keydown', _themeTrap);
const first = _focusable(modal)[0]; const first = _focusable(modal)[0];
if (first) first.focus(); if (first) first.focus();
syncCustomPickers();
document.getElementById('themeBuilderPanel').classList.toggle('hidden', settings.theme !== 'custom');
} }
function closeThemeModal() { function closeThemeModal() {
const overlay = document.getElementById('themeOverlay'); const overlay = document.getElementById('themeOverlay');
@@ -113,7 +115,131 @@ function switchTheme(name) {
*/ */
function isValidBgUrl(url) { function isValidBgUrl(url) {
return typeof url === 'string' && url.length > 0 && 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 ---- // ---- ACCORDION ----
@@ -344,7 +470,11 @@ function applySettings() {
const langEl = document.getElementById('settingLanguage'); const langEl = document.getElementById('settingLanguage');
if (langEl) langEl.value = settings.language || 'auto'; 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)) { if (settings.bgUrl && isValidBgUrl(settings.bgUrl)) {
document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`; document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`;
@@ -371,10 +501,25 @@ function bindSettingsEvents() {
const themeCards = document.querySelectorAll('.theme-card'); const themeCards = document.querySelectorAll('.theme-card');
function selectThemeCard(card) { function selectThemeCard(card) {
const name = card.dataset.value; 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.theme = name;
settings.bgUrl = ''; settings.bgUrl = '';
document.getElementById('bgUrlInput').value = ''; 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 // aria-pressed synchron halten — applyTheme/switchTheme pflegt nur die .active-Klasse, nicht ARIA
themeCards.forEach(c => c.setAttribute('aria-pressed', c === card ? 'true' : 'false')); 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 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 // Accordion initialisieren
initAccordion(); initAccordion();
@@ -434,7 +604,9 @@ function bindSettingsEvents() {
// Background URL (im Theme-Modal) // Background URL (im Theme-Modal)
document.getElementById('btnChangeBg').addEventListener('click', () => { 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 () => { document.getElementById('btnApplyBg').addEventListener('click', async () => {
const url = document.getElementById('bgUrlInput').value.trim(); const url = document.getElementById('bgUrlInput').value.trim();
@@ -458,8 +630,14 @@ function bindSettingsEvents() {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async ev => { reader.onload = async ev => {
if (!isValidBgUrl(ev.target.result)) return; if (!isValidBgUrl(ev.target.result)) return;
settings.bgUrl = ev.target.result; let bg = ev.target.result;
document.getElementById('bgLayer').style.backgroundImage = `url('${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(); await saveSettings();
}; };
reader.onerror = () => { reader.onerror = () => {
@@ -512,7 +690,8 @@ function bindSettingsEvents() {
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false, settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula', hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
showSearch: true, searchEngine: 'google', toolbarPos: 'right', showSearch: true, searchEngine: 'google', toolbarPos: 'right',
imageRefEnabled: false, language: 'auto' }; imageRefEnabled: false, language: 'auto', customTheme: null };
clearCustomTheme();
await saveBoards(); await saveBoards();
await saveTrash(); await saveTrash();
await saveSettings(); await saveSettings();
+1
View File
@@ -25,6 +25,7 @@ let settings = {
visibleCount: 10, visibleCount: 10,
bgUrl: '', bgUrl: '',
theme: 'nebula', theme: 'nebula',
customTheme: null,
showSearch: true, showSearch: true,
searchEngine: 'google', searchEngine: 'google',
toolbarPos: 'right', toolbarPos: 'right',