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) ||