fix(quick-save): Firefox-importScripts-Guard (Event-Page), Sync-Guard auf reale Overlay/Drag-Klassen, Worker-Serialisierung + interne-URL-Filter + kurzer Fehler-Badge

This commit is contained in:
2026-06-14 14:14:31 +02:00
parent a37f34eeac
commit 5feadcc90c
2 changed files with 27 additions and 17 deletions
+6 -6
View File
@@ -241,12 +241,12 @@ function bindStorageSync() {
if (area !== 'local' || !changes.boards) return; if (area !== 'local' || !changes.boards) return;
const next = changes.boards.newValue; const next = changes.boards.newValue;
if (!Array.isArray(next)) return; if (!Array.isArray(next)) return;
// Guard (W-c): nicht mitten in einer offenen Interaktion das boards-Array ersetzen und // Guard (W-c, nach Phase-4-Review auf REALE Klassen korrigiert): nicht mitten in einer offenen
// neu rendern. Ein offener Dialog (Delete/Edit) haelt evtl. eine board-Referenz per Closure; // Interaktion das boards-Array ersetzen und neu rendern, sonst verwaist eine per-Closure gehaltene
// ein laufender Board-Drag (Phase 5) wuerde durch renderBoards() (replaceChildren) abgerissen. // board-Referenz oder ein laufender Drag/Render wird abgerissen. Abgedeckt: Settings (.panel-overlay),
// In dem Fall den Sync verwerfen — der naechste eigene Save/Render zieht den Stand nach. // Modals Add-Board/Add-Bookmark/Rename (.modal-overlay), HellionDialog/Onboarding (.dialog-overlay),
if (document.querySelector('.dialog-overlay.active') || // Board-Drag (.board.dragging), Bookmark-Drag (.bm-item.dragging-source).
document.body.classList.contains('board-dragging')) return; if (document.querySelector('.panel-overlay.active, .modal-overlay.active, .dialog-overlay.active, .board.dragging, .bm-item.dragging-source')) return;
boards = next; boards = next;
renderBoards(); renderBoards();
}); });
+21 -11
View File
@@ -5,10 +5,13 @@
synchron auf Top-Level. Geteilte Logik via importScripts. synchron auf Top-Level. Geteilte Logik via importScripts.
============================================= */ ============================================= */
// Geteiltes DOM-freies Helfer-Modul aus Phase 1: ensureInbox(boards), uid(), // Geteiltes DOM-freies Helfer-Modul aus Phase 1: ensureInbox(boards), uid(), normalizeBookmark(...).
// normalizeBookmark(...). importScripts ist im Service-Worker UND in der // Chrome-Service-Worker laedt es via importScripts. Firefox-Event-Page hat KEIN importScripts —
// Firefox-Event-Page verfuegbar. // dort kommt das Modul ueber background.scripts (manifest.firefox.json) in den Scope, ensureInbox
importScripts('quicksave-core.js'); // ist dann schon definiert. Der Guard verhindert den ReferenceError in der Event-Page.
if (typeof importScripts === 'function' && typeof ensureInbox === 'undefined') {
importScripts('quicksave-core.js');
}
// chrome.storage.local-Lese-/Schreib-Helfer als Promises (kein Store-Modul im Worker, // chrome.storage.local-Lese-/Schreib-Helfer als Promises (kein Store-Modul im Worker,
// das ist DOM/Seiten-gebunden). Identisches Verhalten: get -> Wert oder null. // das ist DOM/Seiten-gebunden). Identisches Verhalten: get -> Wert oder null.
@@ -30,22 +33,26 @@ function bgSet(key, value) {
}); });
} }
// Kurze Badge-Bestaetigung, dann automatisch wieder leeren. // Kurze Badge-Bestaetigung, dann automatisch wieder leeren. color optional (Default gruen).
function flashBadge(text) { function flashBadge(text, color) {
chrome.action.setBadgeText({ text }); chrome.action.setBadgeText({ text });
// Hintergrundfarbe optional, ohne extra Permission moeglich. // Hintergrundfarbe optional, ohne extra Permission moeglich.
if (chrome.action.setBadgeBackgroundColor) { if (chrome.action.setBadgeBackgroundColor) {
chrome.action.setBadgeBackgroundColor({ color: '#1f9d55' }); chrome.action.setBadgeBackgroundColor({ color: color || '#1f9d55' });
} }
setTimeout(() => chrome.action.setBadgeText({ text: '' }), 2000); setTimeout(() => chrome.action.setBadgeText({ text: '' }), 2000);
} }
// Interne/nicht speicherbare Seiten (Browser-UI, Extension-Seiten) — kein sinnvolles Bookmark.
const UNSAVEABLE_URL = /^(chrome|chrome-extension|about|edge|opera|moz-extension|brave|vivaldi|view-source|devtools):/i;
// Quick-Save: aktiven Tab lesen, in die Inbox haengen (read-modify-write). // Quick-Save: aktiven Tab lesen, in die Inbox haengen (read-modify-write).
async function quickSaveActiveTab() { async function quickSaveActiveTab() {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tab = tabs && tabs[0]; const tab = tabs && tabs[0];
if (!tab || !tab.url) { if (!tab || !tab.url || UNSAVEABLE_URL.test(tab.url)) {
flashBadge(chrome.i18n.getMessage('quickSaveNoTab')); // Kein speicherbarer Tab: kurzer roter Marker (langer Text wird im Badge abgeschnitten).
flashBadge('×', '#c0392b');
return; return;
} }
@@ -63,9 +70,12 @@ async function quickSaveActiveTab() {
} }
} }
// Listener SYNCHRON auf Top-Level registrieren (Event-Page/Worker-Anforderung). // Quick-Saves serialisieren: zwei schnelle Tastendruecke (Key-Repeat) wuerden sonst parallel
// read-modify-write machen und sich gegenseitig ueberschreiben (lost update). Promise-Kette
// sorgt fuer sequentielle Ausfuehrung. Listener bleibt SYNCHRON auf Top-Level registriert.
let quickSaveChain = Promise.resolve();
chrome.commands.onCommand.addListener(command => { chrome.commands.onCommand.addListener(command => {
if (command === 'quick-save') { if (command === 'quick-save') {
quickSaveActiveTab(); quickSaveChain = quickSaveChain.then(() => quickSaveActiveTab()).catch(e => console.error('Quick-Save:', e && e.message));
} }
}); });