From de64ae21c0d85ed63cc1c5401f0077cfa87c7c50 Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Mon, 19 Jan 2026 14:55:37 +1300 Subject: [PATCH] Add bookmark management UI and logic Introduces UI buttons for adding bookmarks and current page bookmarks in bigpicture.html. Implements bookmark creation, editing, and saving logic in bigpicture.js, including OSK integration for bookmark input, persistent storage support, and improved rendering of bookmark tiles. --- renderer/bigpicture.html | 10 +++ renderer/bigpicture.js | 168 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 170 insertions(+), 8 deletions(-) diff --git a/renderer/bigpicture.html b/renderer/bigpicture.html index 88a0c0f..b84937d 100644 --- a/renderer/bigpicture.html +++ b/renderer/bigpicture.html @@ -143,6 +143,16 @@

Bookmarks

Your saved websites

+
+ + +
diff --git a/renderer/bigpicture.js b/renderer/bigpicture.js index 2e909cf..5be5862 100644 --- a/renderer/bigpicture.js +++ b/renderer/bigpicture.js @@ -161,6 +161,7 @@ const state = { oskVisible: false, oskCallback: null, oskFocusIndex: 0, + oskContext: null, // Data bookmarks: [], @@ -350,6 +351,17 @@ function initNavigation() { showToast('History refreshed'); }); } + + // Bookmarks actions + const addBookmarkBtn = document.getElementById('addBookmarkBtn'); + if (addBookmarkBtn) { + addBookmarkBtn.addEventListener('click', () => startAddBookmark()); + } + + const addCurrentBookmarkBtn = document.getElementById('addCurrentBookmarkBtn'); + if (addCurrentBookmarkBtn) { + addCurrentBookmarkBtn.addEventListener('click', () => addBookmarkFromCurrentPage()); + } // Settings cards document.querySelectorAll('.settings-card').forEach(card => { @@ -1070,7 +1082,7 @@ function initOSK() { document.querySelector('.osk-close')?.addEventListener('click', () => closeOSK()); } -function openOSK(mode = 'search') { +function openOSK(mode = 'search', options = {}) { const overlay = document.getElementById('osk-overlay'); const input = document.getElementById('osk-input'); const label = document.getElementById('osk-label'); @@ -1081,15 +1093,25 @@ function openOSK(mode = 'search') { state.oskMode = mode; overlay.classList.remove('hidden'); - // Clear input - input.value = ''; + // Set input + input.value = typeof options.initialValue === 'string' ? options.initialValue : ''; // Reset cursor position updateOSKCursorPosition(); // Update label based on mode if (label) { - label.textContent = mode === 'search' ? 'Search or enter URL' : 'Enter text'; + if (options.labelText) { + label.textContent = options.labelText; + } else if (mode === 'search') { + label.textContent = 'Search or enter URL'; + } else if (mode === 'bookmark-url') { + label.textContent = 'Bookmark URL'; + } else if (mode === 'bookmark-title') { + label.textContent = 'Bookmark title'; + } else { + label.textContent = 'Enter text'; + } } // Update focusable elements to only include OSK keys @@ -1216,7 +1238,7 @@ function updateOSKCursorPosition() { cursor.style.left = `${paddingLeft + textWidth}px`; } -function submitOSK() { +async function submitOSK() { const input = document.getElementById('osk-input'); if (!input) return; @@ -1228,6 +1250,27 @@ function submitOSK() { } else if (state.oskMode === 'webview' && state.currentWebview) { // Send the typed text to the webview's focused input sendTextToWebview(value, true); // true = submit after setting + } else if (state.oskMode === 'bookmark-url') { + const normalized = normalizeBookmarkUrl(value); + if (!normalized) { + showToast('Enter a valid URL'); + return; + } + state.oskContext = { url: normalized }; + openOSK('bookmark-title', { + labelText: 'Bookmark title', + initialValue: getDomainFromUrl(normalized) + }); + return; + } else if (state.oskMode === 'bookmark-title') { + const url = state.oskContext?.url; + if (!url) { + closeOSK(); + return; + } + const title = value.trim() || getDomainFromUrl(url); + await addOrUpdateBookmark({ title, url, icon: getFaviconUrl(url) || 'bookmark' }); + state.oskContext = null; } closeOSK(); @@ -1309,7 +1352,9 @@ async function loadData() { async function loadBookmarks() { try { - if (ipcRenderer && ipcRenderer.invoke) { + if (window.bookmarksAPI && typeof window.bookmarksAPI.load === 'function') { + state.bookmarks = await window.bookmarksAPI.load() || []; + } else if (ipcRenderer && ipcRenderer.invoke) { state.bookmarks = await ipcRenderer.invoke('load-bookmarks') || []; } else { // Fallback to localStorage @@ -1323,6 +1368,24 @@ async function loadBookmarks() { } } +async function saveBookmarks(bookmarks) { + try { + if (window.bookmarksAPI && typeof window.bookmarksAPI.save === 'function') { + await window.bookmarksAPI.save(bookmarks); + return true; + } + if (ipcRenderer && ipcRenderer.invoke) { + await ipcRenderer.invoke('save-bookmarks', bookmarks); + return true; + } + localStorage.setItem('bookmarks', JSON.stringify(bookmarks)); + return true; + } catch (err) { + console.error('[BigPicture] Failed to save bookmarks:', err); + return false; + } +} + async function loadHistory() { try { if (ipcRenderer && ipcRenderer.invoke) { @@ -1403,7 +1466,7 @@ function renderQuickAccess() { addTile.dataset.focusable = ''; addTile.tabIndex = 0; addTile.innerHTML = `add`; - addTile.addEventListener('click', () => showToast('Add bookmark coming soon')); + addTile.addEventListener('click', () => startAddBookmark()); grid.appendChild(addTile); updateFocusableElements(); @@ -1420,9 +1483,12 @@ function renderBookmarks() {
bookmark_border

No bookmarks yet

-

Add bookmarks in desktop mode to see them here

+

Add a bookmark here or in desktop mode

`; + const addTile = createAddBookmarkTile(); + grid.appendChild(addTile); + updateFocusableElements(); return; } @@ -1430,10 +1496,23 @@ function renderBookmarks() { const tile = createBookmarkTile(bookmark); grid.appendChild(tile); }); + + const addTile = createAddBookmarkTile(); + grid.appendChild(addTile); updateFocusableElements(); } +function createAddBookmarkTile() { + const addTile = document.createElement('div'); + addTile.className = 'tile add-tile'; + addTile.dataset.focusable = ''; + addTile.tabIndex = 0; + addTile.innerHTML = `bookmark_add`; + addTile.addEventListener('click', () => startAddBookmark()); + return addTile; +} + function createBookmarkTile(bookmark) { const tile = document.createElement('div'); tile.className = 'tile bookmark-tile'; @@ -1467,6 +1546,64 @@ function createBookmarkTile(bookmark) { return tile; } +function startAddBookmark() { + state.oskContext = null; + openOSK('bookmark-url', { labelText: 'Bookmark URL' }); +} + +function addBookmarkFromCurrentPage() { + const webview = state.currentWebview; + if (!webview) { + showToast('No active page to bookmark'); + return; + } + + const url = typeof webview.getURL === 'function' ? webview.getURL() : webview.src; + if (!url) { + showToast('No active page to bookmark'); + return; + } + + const title = typeof webview.getTitle === 'function' ? webview.getTitle() : getDomainFromUrl(url); + addOrUpdateBookmark({ title, url, icon: getFaviconUrl(url) || 'bookmark' }); +} + +async function addOrUpdateBookmark(entry) { + const normalized = normalizeBookmarkUrl(entry.url); + if (!normalized) { + showToast('Enter a valid URL'); + return false; + } + + const title = (entry.title || '').trim() || getDomainFromUrl(normalized); + const icon = entry.icon || getFaviconUrl(normalized) || 'bookmark'; + + const existingIndex = state.bookmarks.findIndex(b => + (b.url || '').toLowerCase() === normalized.toLowerCase() + ); + + if (existingIndex >= 0) { + state.bookmarks[existingIndex] = { + ...state.bookmarks[existingIndex], + title, + url: normalized, + icon + }; + } else { + state.bookmarks.unshift({ title, url: normalized, icon }); + } + + const saved = await saveBookmarks(state.bookmarks); + if (saved) { + renderBookmarks(); + showToast(existingIndex >= 0 ? 'Bookmark updated' : 'Bookmark added'); + } else { + showToast('Failed to save bookmark'); + } + + return saved; +} + function renderHistory() { const list = document.getElementById('historyList'); if (!list) return; @@ -2371,6 +2508,21 @@ async function copyDiagnostics() { // UTILITIES // ============================================================================= +function normalizeBookmarkUrl(raw) { + if (!raw || !raw.trim()) return null; + let url = raw.trim(); + + if (url.startsWith('nebula://')) return url; + + // Add protocol if missing + if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) { + url = `https://${url}`; + } + + if (!isUrl(url)) return null; + return url; +} + function isUrl(str) { // Simple URL detection return /^(https?:\/\/)?[\w-]+(\.[\w-]+)+/.test(str) ||