Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c509647da | |||
| d041c66dfb | |||
| 520a062049 | |||
| 327bcd3385 | |||
| 530196ddf7 | |||
| 17eac64683 | |||
| 1d17f4d11f | |||
| b3288b47eb | |||
| 84976f5a10 | |||
| 5b18bed9b5 |
@@ -45,6 +45,11 @@ jobs:
|
||||
assert m.get('name'), 'Chrome: Name fehlt'
|
||||
assert m.get('version'), 'Chrome: Version fehlt'
|
||||
assert 'storage' in m.get('permissions', []), 'Chrome: Storage Permission fehlt'
|
||||
assert 'activeTab' in m.get('permissions', []), 'Chrome: activeTab Permission fehlt (Quick Save v2.3)'
|
||||
assert 'background' in m, 'Chrome: background-Key fehlt (Service Worker v2.3)'
|
||||
assert 'service_worker' in m.get('background', {}), 'Chrome: background.service_worker fehlt'
|
||||
assert isinstance(m.get('commands'), dict) and 'quick-save' in m['commands'], 'Chrome: commands.quick-save fehlt (Quick Save v2.3)'
|
||||
assert 'action' in m, 'Chrome: action-Key fehlt (Badge-Bestätigung v2.3)'
|
||||
print('manifest.json (V3) OK — Version:', m['version'])
|
||||
|
||||
with open('manifest.firefox.json') as f:
|
||||
@@ -52,6 +57,11 @@ jobs:
|
||||
assert mf.get('manifest_version') == 3, 'Firefox: Manifest V3 erwartet'
|
||||
assert mf['version'] == m['version'], 'Firefox: Version stimmt nicht mit Chrome überein!'
|
||||
assert 'browser_specific_settings' in mf, 'Firefox: browser_specific_settings fehlt'
|
||||
assert 'activeTab' in mf.get('permissions', []), 'Firefox: activeTab Permission fehlt (Quick Save v2.3)'
|
||||
assert 'background' in mf, 'Firefox: background-Key fehlt (Event-Page v2.3)'
|
||||
assert 'scripts' in mf.get('background', {}), 'Firefox: background.scripts fehlt (Event-Page, kein service_worker)'
|
||||
assert isinstance(mf.get('commands'), dict) and 'quick-save' in mf['commands'], 'Firefox: commands.quick-save fehlt (Quick Save v2.3)'
|
||||
assert 'action' in mf, 'Firefox: action-Key fehlt (Badge-Bestätigung v2.3)'
|
||||
print('manifest.firefox.json (V3) OK — Version:', mf['version'])
|
||||
|
||||
with open('manifest.opera.json') as f:
|
||||
|
||||
@@ -6,6 +6,20 @@ All notable changes per version. Format based on [Keep a Changelog](https://keep
|
||||
|
||||
---
|
||||
|
||||
## [2.3.0] — 2026-06-14
|
||||
|
||||
### Added
|
||||
- **Command Palette (Ctrl+K)** — Overlay that live-filters bookmarks (title and URL) and board names from the keyboard. Arrow-key navigation, Enter opens the match, Escape closes. Read-only navigation, separate from the web search bar. Combobox/listbox ARIA pattern with focus trap and focus return. New DE/EN i18n strings.
|
||||
- **Trash** — Deleted bookmarks and boards move to a 30-day trash instead of vanishing. Restore or permanently remove them from a new Settings section; entries older than 30 days are cleaned up automatically. Stored under its own storage key with a hard size cap so it cannot exhaust the storage quota.
|
||||
- **Quick Save** — A global keyboard shortcut (default Alt+Shift+S, configurable in the browser shortcut settings) saves the current tab into a fixed Inbox board from any page. Backed by a background worker (service worker on Chrome/Opera, event page on Firefox) that appends to a dedicated pending queue, which the dashboard drains into the Inbox — separate write domains, so a save can never clobber the boards. A badge confirms the save, and open dashboard tabs sync the new bookmark live via a storage-change listener.
|
||||
- **Free layout (bonus)** — Boards can be dragged to free positions via a drag handle, persisted per board. Positions are clamped back into view when the window shrinks, and the layout falls back to a stacked column on small screens. Each board can be pinned with a lock button: a locked board cannot be moved (its drag handle is hidden), preventing accidental repositioning. A drag only counts past a small movement threshold, so a mere click on the handle never shifts a board.
|
||||
|
||||
### Changed
|
||||
- The bookmark- and board-delete paths no longer remove entries immediately; deletions now route through the trash.
|
||||
- Chrome and Firefox manifests gain a background worker, an `action` entry and the `activeTab` / `commands` permissions to support Quick Save. Opera keeps its existing `tabs` permission and redirect worker.
|
||||
|
||||
---
|
||||
|
||||
## [2.2.0] — 2026-06-13
|
||||
|
||||
### Added
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extName__",
|
||||
"default_locale": "en",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"description": "__MSG_extDesc__",
|
||||
"author": "Hellion Online Media - Florian Wathling",
|
||||
"homepage_url": "https://hellion-media.de",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extName__",
|
||||
"default_locale": "en",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"description": "__MSG_extDesc__",
|
||||
"author": "Hellion Online Media - Florian Wathling",
|
||||
"homepage_url": "https://hellion-media.de",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extName__",
|
||||
"default_locale": "en",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"description": "__MSG_extDesc__",
|
||||
"author": "Hellion Online Media - Florian Wathling",
|
||||
"homepage_url": "https://hellion-media.de",
|
||||
|
||||
+1
-1
@@ -241,7 +241,7 @@
|
||||
<div class="panel-footer">
|
||||
<div class="about-block">
|
||||
<div class="about-logo" data-i18n="about.title">⬡ HELLION NEWTAB</div>
|
||||
<div class="about-version">Version 2.2.0 · by Hellion Online Media</div>
|
||||
<div class="about-version">Version 2.3.0 · by Hellion Online Media</div>
|
||||
|
||||
<div class="about-links">
|
||||
<a href="https://hellion-media.de/impressum" target="_blank" class="about-link">
|
||||
|
||||
@@ -568,9 +568,18 @@ html, body {
|
||||
flex-shrink: 0;
|
||||
transition: color 0.15s;
|
||||
touch-action: none;
|
||||
/* Ueber dem Blur-Overlay (.board-blur-overlay, z-index:5) liegen, sonst schluckt das
|
||||
Overlay den pointerdown und ein geblurrtes Board ist nicht verschiebbar. Der Rest des
|
||||
Boards bleibt unter dem Overlay -> Klick dort enthuellt weiterhin (Phase-5-Review). */
|
||||
position: relative;
|
||||
z-index: 6;
|
||||
}
|
||||
.board-drag-handle:hover { color: var(--accent); }
|
||||
.board-drag-handle:active { cursor: grabbing; }
|
||||
/* Gesperrtes Board (Position fixiert): Drag-Handle ausblenden (kein Verschieben mehr),
|
||||
Lock-Button aktiv einfaerben. Hoehere Spezifitaet als .board-drag-handle -> display:none gewinnt. */
|
||||
.board.locked .board-drag-handle { display: none; }
|
||||
.board.locked .btn-lock-board { color: var(--accent); }
|
||||
|
||||
.board-title {
|
||||
font-family: var(--font-display);
|
||||
|
||||
+31
-5
@@ -119,7 +119,7 @@ async function checkBackupReminder() {
|
||||
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
|
||||
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
|
||||
const timerPresets = (widgetData && widgetData.timer) ? widgetData.timer.presets || [] : [];
|
||||
const data = { version: '2.2.0', exported: new Date().toISOString(), boards, settings, trash, notes: notesData, calculator: calcHistory, timerPresets };
|
||||
const data = { version: '2.3.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 url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -241,6 +241,7 @@ function bindGlobalEvents() {
|
||||
// gegenseitig ueberschreiben (Datensicherheit, Phase-4-Review-Blocker 2b).
|
||||
let _drainBusy = false;
|
||||
let _drainQueued = false; // ein waehrend eines laufenden Drains angefragter Drain wird nachgeholt
|
||||
let _renderDeferredByDrag = false; // Drain hat den Render wegen eines laufenden Drags ausgelassen -> nach Drag-Ende nachholen
|
||||
async function drainQuickSavePending() {
|
||||
if (_drainBusy) { _drainQueued = true; return; }
|
||||
_drainBusy = true;
|
||||
@@ -250,10 +251,19 @@ async function drainQuickSavePending() {
|
||||
const drained = pending.slice();
|
||||
const drainedIds = new Set(drained.map(e => e && e.id).filter(Boolean));
|
||||
const inbox = await ensureInboxBoard(); // legt die Inbox an, falls noetig; gibt das Board zurueck
|
||||
// Idempotenz gegen den Worker/Drain-Race auf 'quicksave_pending': jede eingespielte Inbox-
|
||||
// Bookmark traegt die Pending-id ihres Ursprungs als srcId. Taucht ein bereits gedrainter
|
||||
// Eintrag durch einen gleichzeitigen Worker-Append erneut in der Queue auf, wird er hier
|
||||
// uebersprungen statt doppelt eingefuegt — kein Duplikat, und kein Verlust (boards-Write
|
||||
// bleibt vor der Queue-Bereinigung, daher keine umgekehrte Verlustgefahr).
|
||||
const seenSrc = new Set(inbox.bookmarks.map(b => b && b.srcId).filter(Boolean));
|
||||
for (const e of drained) {
|
||||
if (e && typeof e.url === 'string' && e.url) {
|
||||
inbox.bookmarks.push(normalizeBookmark({ title: e.title, url: e.url }));
|
||||
}
|
||||
if (!e || !e.id || seenSrc.has(e.id)) continue; // schon eingespielt
|
||||
if (typeof e.url !== 'string' || !e.url || !isSafeUrl(e.url)) continue; // leeres/unsicheres Protokoll verwerfen
|
||||
const bm = normalizeBookmark({ title: e.title, url: e.url });
|
||||
bm.srcId = e.id; // Herkunft fuer kuenftige Dedup
|
||||
inbox.bookmarks.push(bm);
|
||||
seenSrc.add(e.id);
|
||||
}
|
||||
await saveBoards();
|
||||
// NUR die verarbeiteten Eintraege entfernen — ein gleichzeitiger Worker-Append bleibt erhalten.
|
||||
@@ -261,7 +271,14 @@ async function drainQuickSavePending() {
|
||||
const remaining = Array.isArray(still) ? still.filter(e => e && !drainedIds.has(e.id)) : [];
|
||||
await Store.set('quicksave_pending', remaining);
|
||||
// Render nur, wenn gerade KEIN Drag laeuft (renderBoards->replaceChildren wuerde ihn abreissen).
|
||||
if (!document.querySelector('.board.dragging, .bm-item.dragging-source')) renderBoards();
|
||||
// Laeuft einer, den Render-Wunsch merken und nach Drag-Ende nachholen (drag.js ruft
|
||||
// flushQuickSaveRenderIfDeferred), sonst bliebe der frisch gedrainte Quick-Save bis zu einem
|
||||
// unabhaengigen Fremd-Render unsichtbar (Phase-6-Review).
|
||||
if (document.querySelector('.board.dragging, .bm-item.dragging-source')) {
|
||||
_renderDeferredByDrag = true;
|
||||
} else {
|
||||
renderBoards();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Quick-Save-Drain fehlgeschlagen:', e && e.message);
|
||||
@@ -273,6 +290,15 @@ async function drainQuickSavePending() {
|
||||
if (_drainQueued) { _drainQueued = false; drainQuickSavePending(); }
|
||||
}
|
||||
|
||||
// Wird von drag.js nach jedem Drag-Ende aufgerufen: einen waehrend des Drags ausgelassenen
|
||||
// Quick-Save-Render nachholen. Idempotent — tut nichts, wenn kein Render aussteht.
|
||||
function flushQuickSaveRenderIfDeferred() {
|
||||
if (_renderDeferredByDrag) {
|
||||
_renderDeferredByDrag = false;
|
||||
renderBoards();
|
||||
}
|
||||
}
|
||||
|
||||
// Live-Sync (QS-03): ein offener NewTab drained die Queue, sobald der Worker etwas anhaengt.
|
||||
function bindStorageSync() {
|
||||
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.onChanged) return;
|
||||
|
||||
+25
-3
@@ -46,6 +46,13 @@ function createPlusSvg() {
|
||||
]);
|
||||
}
|
||||
|
||||
/** Erzeugt das Pin-/Reisszwecke-Icon SVG (Position fixieren) — bewusst KEIN Emoji (custom SVG). */
|
||||
function createPinSvg() {
|
||||
return svgEl('svg', { width: '11', height: '12', viewBox: '0 0 24 24', fill: 'currentColor' }, [
|
||||
svgEl('path', { d: 'M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z' }),
|
||||
]);
|
||||
}
|
||||
|
||||
// ---- POS-MIGRATION ----
|
||||
// Boards ohne pos (Altbestand vor v2.3) aus einem Auto-Raster befuellen,
|
||||
// damit sie sich nicht alle auf (0,0) stapeln. Raster orientiert sich am
|
||||
@@ -113,7 +120,7 @@ function renderBoards() {
|
||||
|
||||
function createBoardEl(board) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'board' + (board.blurred ? ' blurred' : '');
|
||||
div.className = 'board' + (board.blurred ? ' blurred' : '') + (board.locked ? ' locked' : '');
|
||||
div.dataset.boardId = board.id;
|
||||
|
||||
// Header
|
||||
@@ -133,6 +140,11 @@ function createBoardEl(board) {
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'board-actions';
|
||||
|
||||
const btnLock = document.createElement('button');
|
||||
btnLock.className = 'board-action-btn btn-lock-board';
|
||||
btnLock.title = board.locked ? t('boards.unlock') : t('boards.lock');
|
||||
btnLock.appendChild(createPinSvg());
|
||||
|
||||
const btnBlur = document.createElement('button');
|
||||
btnBlur.className = 'board-action-btn btn-blur-board';
|
||||
btnBlur.title = board.blurred ? t('boards.unblur') : t('boards.blur');
|
||||
@@ -152,9 +164,9 @@ function createBoardEl(board) {
|
||||
}
|
||||
|
||||
if (btnDelete) {
|
||||
actions.append(btnBlur, btnRename, btnDelete);
|
||||
actions.append(btnLock, btnBlur, btnRename, btnDelete);
|
||||
} else {
|
||||
actions.append(btnBlur, btnRename);
|
||||
actions.append(btnLock, btnBlur, btnRename);
|
||||
}
|
||||
header.append(dragHandle, titleSpanHeader, actions);
|
||||
|
||||
@@ -163,6 +175,16 @@ function createBoardEl(board) {
|
||||
blurOverlay.className = 'board-blur-overlay';
|
||||
div.appendChild(blurOverlay);
|
||||
|
||||
btnLock.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
// Position fixieren: blendet via .board.locked den Drag-Handle aus (CSS) und der onDown-Guard
|
||||
// in drag.js verweigert zusaetzlich den Drag. Reiner Klassen-Toggle, kein Re-Render noetig.
|
||||
board.locked = !board.locked;
|
||||
div.classList.toggle('locked', board.locked);
|
||||
btnLock.title = board.locked ? t('boards.unlock') : t('boards.lock');
|
||||
await saveBoards();
|
||||
});
|
||||
|
||||
btnBlur.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
board.blurred = !board.blurred;
|
||||
|
||||
+15
-7
@@ -39,7 +39,7 @@ function initDataButtons() {
|
||||
btnExport.addEventListener('click', async () => {
|
||||
const widgetData = await Store.get('widgetStates');
|
||||
const data = {
|
||||
version: '2.2.0',
|
||||
version: '2.3.0',
|
||||
exported: new Date().toISOString(),
|
||||
boards,
|
||||
settings,
|
||||
@@ -72,6 +72,7 @@ function initDataButtons() {
|
||||
id: b.id || uid(),
|
||||
title: String(b.title).slice(0, 100),
|
||||
blurred: !!b.blurred,
|
||||
locked: !!b.locked,
|
||||
bookmarks: b.bookmarks
|
||||
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
||||
.map(bm => ({
|
||||
@@ -109,6 +110,7 @@ function initDataButtons() {
|
||||
id: e.item.id || uid(),
|
||||
title: String(e.item.title || '').slice(0, 100),
|
||||
blurred: !!e.item.blurred,
|
||||
locked: !!e.item.locked,
|
||||
// Position erhalten, damit ein wiederhergestelltes Board an seinem Platz landet.
|
||||
...(safePos(e.item.pos) ? { pos: safePos(e.item.pos) } : {}),
|
||||
bookmarks: Array.isArray(e.item.bookmarks)
|
||||
@@ -124,12 +126,18 @@ function initDataButtons() {
|
||||
}))
|
||||
.filter(e => e.item !== null);
|
||||
if (validTrash.length > 0) {
|
||||
// Nach deletedAt aufsteigend sortieren, DANN die neuesten TRASH_MAX_ENTRIES behalten.
|
||||
// Positionsbasiertes slice(-N) wuerde sonst frische lokale Eintraege verdraengen
|
||||
// statt der aeltesten — Datenverlust, da ein Trash-Eintrag die einzige Kopie ist.
|
||||
const combined = [...trash, ...validTrash];
|
||||
combined.sort((a, b) => a.deletedAt - b.deletedAt);
|
||||
trash = combined.slice(-TRASH_MAX_ENTRIES);
|
||||
// Lokale Eintraege sind die EINZIGE Kopie ihrer geloeschten Daten -> Vorrang. Importierte
|
||||
// stammen aus einem Backup, das der Nutzer noch besitzt -> nachrangig. Daher: erst ALLE
|
||||
// lokalen behalten (pushToTrash kappt sie bereits auf TRASH_MAX_ENTRIES), dann mit den
|
||||
// NEUESTEN importierten bis zur Obergrenze auffuellen. Ein frischer Import verdraengt so
|
||||
// keine aelteren lokalen Sole-Copies mehr (frueheres sort+slice(-N) konnte das, data-loss).
|
||||
const room = Math.max(0, TRASH_MAX_ENTRIES - trash.length);
|
||||
const keptImports = validTrash
|
||||
.slice()
|
||||
.sort((a, b) => b.deletedAt - a.deletedAt) // neueste Importe zuerst
|
||||
.slice(0, room);
|
||||
// Am Ende nach deletedAt aufsteigend fuer eine stabile Anzeige-Reihenfolge.
|
||||
trash = [...trash, ...keptImports].sort((a, b) => a.deletedAt - b.deletedAt);
|
||||
await saveTrash();
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -21,6 +21,9 @@ function initBoardDragDrop() {
|
||||
handle.addEventListener('pointerdown', function onDown(e) {
|
||||
// Auf Mobil ist .board position:static (Stapel) -> kein Free-Move.
|
||||
if (getComputedStyle(boardEl).position !== 'absolute') return;
|
||||
// Gesperrtes Board (Position fixiert, LAYOUT-LOCK) nicht verschieben. Der Drag-Handle ist
|
||||
// bei .locked schon per CSS ausgeblendet; dieser Guard ist die zweite Sicherung.
|
||||
if (boardEl.classList.contains('locked')) return;
|
||||
e.preventDefault();
|
||||
handle.setPointerCapture(e.pointerId);
|
||||
// .board.dragging hebt das Board per CSS nach vorne (z-index) UND ist das Signal fuer den
|
||||
@@ -31,8 +34,16 @@ function initBoardDragDrop() {
|
||||
const rect = boardEl.getBoundingClientRect();
|
||||
const offX = e.clientX - rect.left;
|
||||
const offY = e.clientY - rect.top;
|
||||
const startCX = e.clientX, startCY = e.clientY;
|
||||
// Erst eine echte Bewegung (> 3px) zaehlt als Drag. Ein reiner Klick/Tap auf den Handle darf
|
||||
// board.pos NICHT ueberschreiben: renderBoards() schreibt in --board-x/y den gegen die Viewport
|
||||
// GECLAMPTEN Wert, board.pos bleibt absichtlich der wahre (evtl. off-screen) Wert. onUp liest
|
||||
// --board-x/y zurueck — bei einem No-Move-Klick waere das der Clamp und wuerde die wahre
|
||||
// Position zerstoeren (Phase-5-Review, HIGH/data-loss).
|
||||
let moved = false;
|
||||
|
||||
function onMove(ev) {
|
||||
if (Math.abs(ev.clientX - startCX) > 3 || Math.abs(ev.clientY - startCY) > 3) moved = true;
|
||||
const maxX = window.innerWidth - boardEl.offsetWidth;
|
||||
const maxY = window.innerHeight - boardEl.offsetHeight;
|
||||
const x = Math.max(0, Math.min(maxX, ev.clientX - offX));
|
||||
@@ -54,6 +65,8 @@ function initBoardDragDrop() {
|
||||
|
||||
async function onUp() {
|
||||
cleanup();
|
||||
// Nur bei echtem Verschieben persistieren — sonst board.pos unangetastet lassen.
|
||||
if (moved) {
|
||||
const id = boardEl.dataset.boardId;
|
||||
const board = boards.find(b => b.id === id);
|
||||
if (board) {
|
||||
@@ -64,10 +77,16 @@ function initBoardDragDrop() {
|
||||
await saveBoards();
|
||||
}
|
||||
}
|
||||
// Einen waehrend des Drags ausgelassenen Quick-Save-Render nachholen (app.js).
|
||||
if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred();
|
||||
}
|
||||
|
||||
// pointercancel feuert STATT pointerup bei Touch-Interrupt, Browser-Geste oder wenn das
|
||||
// captured Element aus dem DOM faellt -> nur aufraeumen (cleanup), pos NICHT ueberschreiben.
|
||||
function onCancel() { cleanup(); }
|
||||
function onCancel() {
|
||||
cleanup();
|
||||
if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred();
|
||||
}
|
||||
|
||||
handle.addEventListener('pointermove', onMove);
|
||||
handle.addEventListener('pointerup', onUp);
|
||||
@@ -91,6 +110,8 @@ function initBookmarkDragDrop(listEl, board) {
|
||||
listEl.addEventListener('dragend', e => {
|
||||
const item = e.target.closest('.bm-item');
|
||||
if (item) item.classList.remove('dragging-source');
|
||||
// Blieb ein Quick-Save-Render waehrend des Bookmark-Drags aus (Drop ausgeblieben), jetzt nachholen.
|
||||
if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred();
|
||||
});
|
||||
|
||||
listEl.addEventListener('dragover', e => {
|
||||
|
||||
@@ -21,6 +21,8 @@ const STRINGS = {
|
||||
'boards.drag_title': 'Board verschieben',
|
||||
'boards.blur': 'Blur (privat)',
|
||||
'boards.unblur': 'Unblur',
|
||||
'boards.lock': 'Position sperren',
|
||||
'boards.unlock': 'Position entsperren',
|
||||
'boards.rename': 'Umbenennen',
|
||||
'boards.delete': 'Löschen',
|
||||
'boards.delete_confirm': 'Board „{title}" wirklich löschen?',
|
||||
@@ -480,6 +482,8 @@ const STRINGS = {
|
||||
'boards.drag_title': 'Move board',
|
||||
'boards.blur': 'Blur (private)',
|
||||
'boards.unblur': 'Unblur',
|
||||
'boards.lock': 'Lock position',
|
||||
'boards.unlock': 'Unlock position',
|
||||
'boards.rename': 'Rename',
|
||||
'boards.delete': 'Delete',
|
||||
'boards.delete_confirm': 'Really delete board "{title}"?',
|
||||
|
||||
@@ -34,6 +34,17 @@
|
||||
return inbox;
|
||||
}
|
||||
|
||||
// Sicheres URL-Protokoll (http/https/ftp). Inhaltlich identisch zur data.js-Variante, aber
|
||||
// DOM-frei und auf globalThis, damit der Quick-Save-Drain (app.js) dieselbe Validierung nutzt
|
||||
// wie jeder andere Bookmark-Schreibpfad. URL ist in Worker UND Seite verfuegbar.
|
||||
function isSafeUrl(url) {
|
||||
try {
|
||||
return ['http:', 'https:', 'ftp:'].includes(new URL(url).protocol);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalisiert eine Bookmark in die kanonische Form { id, title, url, desc }.
|
||||
// title-Fallback auf url, desc auf ''. Begrenzt Laengen wie data.js (200/500),
|
||||
// damit Quick-Save-Eintraege das gleiche Schema wie Import/Manuell haben.
|
||||
@@ -52,6 +63,7 @@
|
||||
|
||||
root.INBOX_ID = INBOX_ID;
|
||||
root.uid = uid;
|
||||
root.isSafeUrl = isSafeUrl;
|
||||
root.ensureInbox = ensureInbox;
|
||||
root.normalizeBookmark = normalizeBookmark;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : self);
|
||||
|
||||
@@ -54,6 +54,7 @@ function getDefaultBoards() {
|
||||
{ id: uid(), title: 'Next.js Docs', url: 'https://nextjs.org/docs', desc: '' },
|
||||
],
|
||||
blurred: false,
|
||||
locked: false,
|
||||
pos: { x: 40, y: 110 }
|
||||
}
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user