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:
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user