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
This commit is contained in:
2026-06-14 21:34:34 +02:00
parent 42e3cf0dec
commit 9beeec3182
7 changed files with 59 additions and 12 deletions
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+2 -1
View File
@@ -381,9 +381,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>
+4
View File
@@ -2387,6 +2387,10 @@ 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;
}
/* ============================================ /* ============================================
ACCORDION SETTINGS ACCORDION SETTINGS
+8 -4
View File
@@ -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
@@ -846,7 +848,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 +861,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
+42 -4
View File
@@ -113,7 +113,37 @@ 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://'));
}
// 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 ----
@@ -434,7 +464,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 +490,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 = () => {