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.
This commit is contained in:
@@ -143,6 +143,16 @@
|
|||||||
<h1 class="section-title">Bookmarks</h1>
|
<h1 class="section-title">Bookmarks</h1>
|
||||||
<p class="section-subtitle">Your saved websites</p>
|
<p class="section-subtitle">Your saved websites</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section-actions">
|
||||||
|
<button class="action-btn" id="addBookmarkBtn" data-focusable tabindex="0">
|
||||||
|
<span class="material-symbols-outlined">bookmark_add</span>
|
||||||
|
<span>Add Bookmark</span>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn" id="addCurrentBookmarkBtn" data-focusable tabindex="0">
|
||||||
|
<span class="material-symbols-outlined">bookmark</span>
|
||||||
|
<span>Add Current Page</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="tile-grid large" id="bookmarksGrid">
|
<div class="tile-grid large" id="bookmarksGrid">
|
||||||
<!-- Bookmarks will be populated dynamically -->
|
<!-- Bookmarks will be populated dynamically -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+160
-8
@@ -161,6 +161,7 @@ const state = {
|
|||||||
oskVisible: false,
|
oskVisible: false,
|
||||||
oskCallback: null,
|
oskCallback: null,
|
||||||
oskFocusIndex: 0,
|
oskFocusIndex: 0,
|
||||||
|
oskContext: null,
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
bookmarks: [],
|
bookmarks: [],
|
||||||
@@ -351,6 +352,17 @@ function initNavigation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Settings cards
|
||||||
document.querySelectorAll('.settings-card').forEach(card => {
|
document.querySelectorAll('.settings-card').forEach(card => {
|
||||||
card.addEventListener('click', () => {
|
card.addEventListener('click', () => {
|
||||||
@@ -1070,7 +1082,7 @@ function initOSK() {
|
|||||||
document.querySelector('.osk-close')?.addEventListener('click', () => closeOSK());
|
document.querySelector('.osk-close')?.addEventListener('click', () => closeOSK());
|
||||||
}
|
}
|
||||||
|
|
||||||
function openOSK(mode = 'search') {
|
function openOSK(mode = 'search', options = {}) {
|
||||||
const overlay = document.getElementById('osk-overlay');
|
const overlay = document.getElementById('osk-overlay');
|
||||||
const input = document.getElementById('osk-input');
|
const input = document.getElementById('osk-input');
|
||||||
const label = document.getElementById('osk-label');
|
const label = document.getElementById('osk-label');
|
||||||
@@ -1081,15 +1093,25 @@ function openOSK(mode = 'search') {
|
|||||||
state.oskMode = mode;
|
state.oskMode = mode;
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
|
|
||||||
// Clear input
|
// Set input
|
||||||
input.value = '';
|
input.value = typeof options.initialValue === 'string' ? options.initialValue : '';
|
||||||
|
|
||||||
// Reset cursor position
|
// Reset cursor position
|
||||||
updateOSKCursorPosition();
|
updateOSKCursorPosition();
|
||||||
|
|
||||||
// Update label based on mode
|
// Update label based on mode
|
||||||
if (label) {
|
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
|
// Update focusable elements to only include OSK keys
|
||||||
@@ -1216,7 +1238,7 @@ function updateOSKCursorPosition() {
|
|||||||
cursor.style.left = `${paddingLeft + textWidth}px`;
|
cursor.style.left = `${paddingLeft + textWidth}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitOSK() {
|
async function submitOSK() {
|
||||||
const input = document.getElementById('osk-input');
|
const input = document.getElementById('osk-input');
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
@@ -1228,6 +1250,27 @@ function submitOSK() {
|
|||||||
} else if (state.oskMode === 'webview' && state.currentWebview) {
|
} else if (state.oskMode === 'webview' && state.currentWebview) {
|
||||||
// Send the typed text to the webview's focused input
|
// Send the typed text to the webview's focused input
|
||||||
sendTextToWebview(value, true); // true = submit after setting
|
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();
|
closeOSK();
|
||||||
@@ -1309,7 +1352,9 @@ async function loadData() {
|
|||||||
|
|
||||||
async function loadBookmarks() {
|
async function loadBookmarks() {
|
||||||
try {
|
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') || [];
|
state.bookmarks = await ipcRenderer.invoke('load-bookmarks') || [];
|
||||||
} else {
|
} else {
|
||||||
// Fallback to localStorage
|
// 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() {
|
async function loadHistory() {
|
||||||
try {
|
try {
|
||||||
if (ipcRenderer && ipcRenderer.invoke) {
|
if (ipcRenderer && ipcRenderer.invoke) {
|
||||||
@@ -1403,7 +1466,7 @@ function renderQuickAccess() {
|
|||||||
addTile.dataset.focusable = '';
|
addTile.dataset.focusable = '';
|
||||||
addTile.tabIndex = 0;
|
addTile.tabIndex = 0;
|
||||||
addTile.innerHTML = `<span class="material-symbols-outlined">add</span>`;
|
addTile.innerHTML = `<span class="material-symbols-outlined">add</span>`;
|
||||||
addTile.addEventListener('click', () => showToast('Add bookmark coming soon'));
|
addTile.addEventListener('click', () => startAddBookmark());
|
||||||
grid.appendChild(addTile);
|
grid.appendChild(addTile);
|
||||||
|
|
||||||
updateFocusableElements();
|
updateFocusableElements();
|
||||||
@@ -1420,9 +1483,12 @@ function renderBookmarks() {
|
|||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<span class="material-symbols-outlined">bookmark_border</span>
|
<span class="material-symbols-outlined">bookmark_border</span>
|
||||||
<p>No bookmarks yet</p>
|
<p>No bookmarks yet</p>
|
||||||
<p class="empty-hint">Add bookmarks in desktop mode to see them here</p>
|
<p class="empty-hint">Add a bookmark here or in desktop mode</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
const addTile = createAddBookmarkTile();
|
||||||
|
grid.appendChild(addTile);
|
||||||
|
updateFocusableElements();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1431,9 +1497,22 @@ function renderBookmarks() {
|
|||||||
grid.appendChild(tile);
|
grid.appendChild(tile);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const addTile = createAddBookmarkTile();
|
||||||
|
grid.appendChild(addTile);
|
||||||
|
|
||||||
updateFocusableElements();
|
updateFocusableElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createAddBookmarkTile() {
|
||||||
|
const addTile = document.createElement('div');
|
||||||
|
addTile.className = 'tile add-tile';
|
||||||
|
addTile.dataset.focusable = '';
|
||||||
|
addTile.tabIndex = 0;
|
||||||
|
addTile.innerHTML = `<span class="material-symbols-outlined">bookmark_add</span>`;
|
||||||
|
addTile.addEventListener('click', () => startAddBookmark());
|
||||||
|
return addTile;
|
||||||
|
}
|
||||||
|
|
||||||
function createBookmarkTile(bookmark) {
|
function createBookmarkTile(bookmark) {
|
||||||
const tile = document.createElement('div');
|
const tile = document.createElement('div');
|
||||||
tile.className = 'tile bookmark-tile';
|
tile.className = 'tile bookmark-tile';
|
||||||
@@ -1467,6 +1546,64 @@ function createBookmarkTile(bookmark) {
|
|||||||
return tile;
|
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() {
|
function renderHistory() {
|
||||||
const list = document.getElementById('historyList');
|
const list = document.getElementById('historyList');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
@@ -2371,6 +2508,21 @@ async function copyDiagnostics() {
|
|||||||
// UTILITIES
|
// 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) {
|
function isUrl(str) {
|
||||||
// Simple URL detection
|
// Simple URL detection
|
||||||
return /^(https?:\/\/)?[\w-]+(\.[\w-]+)+/.test(str) ||
|
return /^(https?:\/\/)?[\w-]+(\.[\w-]+)+/.test(str) ||
|
||||||
|
|||||||
Reference in New Issue
Block a user