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
+4
View File
@@ -2387,6 +2387,10 @@ 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;
}
/* ============================================
ACCORDION SETTINGS
+8 -4
View File
@@ -385,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',
@@ -396,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
@@ -846,7 +848,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',
@@ -857,7 +861,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
+42 -4
View File
@@ -113,7 +113,37 @@ 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://'));
}
// 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 ----
@@ -434,7 +464,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 +490,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 = () => {