let bookmarks = []; let config = {}; let filteredBookmarks = []; let selectedIndex = 0; let currentCommandSuggestions = []; const searchInput = document.getElementById('search-input'); const resultsContainer = document.getElementById('results'); const STORAGE_KEYS = { CONFIG: 'startpageConfig', BOOKMARKS: 'startpageBookmarks' }; const DEFAULT_CONFIG = { backgroundColor: '#000000', textColor: '#ffffff', accentColor: '#4a9eff', searchEngine: 'https://kagi.com/search?q=', backgroundImage: '/Users/leonversteeg/Downloads/ChatGPT Image Feb 25, 2026, 02_14_59 PM.png', backgroundBlur: 0, maskColor: '#000000', maskOpacity: 80 }; const STYLE_CONSTANTS = { HOVER_OPACITY: 0.1, SELECTED_OPACITY: 0.2 }; /** * Loads data from localStorage or data.json file. * Falls back to default values if both sources fail. */ async function loadData() { // Try localStorage first const savedConfig = localStorage.getItem(STORAGE_KEYS.CONFIG); const savedBookmarks = localStorage.getItem(STORAGE_KEYS.BOOKMARKS); /** if (savedConfig && savedBookmarks) { config = JSON.parse(savedConfig); bookmarks = JSON.parse(savedBookmarks); return; }*/ // If localStorage is incomplete, try loading from data.json try { const response = await fetch('data.json'); const data = await response.json(); config = data.config || DEFAULT_CONFIG; bookmarks = data.bookmarks || []; } catch (error) { console.error('Failed to load data.json:', error); config = DEFAULT_CONFIG; bookmarks = []; } } function getOrCreateStyleElement(id) { let element = document.getElementById(id); if (!element) { element = document.createElement('style'); element.id = id; document.head.appendChild(element); } return element; } function removeElementById(id) { const element = document.getElementById(id); if (element) { element.remove(); } } function applyBackgroundImage(backgroundImage, backgroundBlur) { const blur = backgroundBlur || DEFAULT_CONFIG.backgroundBlur; const filterValue = blur > 0 ? `blur(${blur}px)` : 'none'; const bgStyle = getOrCreateStyleElement('background-style'); bgStyle.textContent = ` body::before { content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-image: url('${backgroundImage}'); background-size: cover; background-position: center; background-repeat: no-repeat; filter: ${filterValue}; z-index: -2; } `; document.body.style.backgroundImage = 'none'; } function applyMask(maskColor, maskOpacity) { removeElementById('background-mask'); const opacity = (maskOpacity || DEFAULT_CONFIG.maskOpacity) / 100; if (opacity > 0) { const mask = document.createElement('div'); mask.id = 'background-mask'; Object.assign(mask.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: maskColor || DEFAULT_CONFIG.maskColor, opacity: opacity.toString(), zIndex: '-1', pointerEvents: 'none' }); document.body.appendChild(mask); } } function applyConfig() { const { backgroundColor, textColor, accentColor, backgroundImage, backgroundBlur, maskColor, maskOpacity } = config; document.body.style.backgroundColor = backgroundColor || DEFAULT_CONFIG.backgroundColor; document.body.style.color = textColor || DEFAULT_CONFIG.textColor; searchInput.style.color = textColor || DEFAULT_CONFIG.textColor; if (backgroundImage) { applyBackgroundImage(backgroundImage, backgroundBlur); applyMask(maskColor, maskOpacity); } else { document.body.style.backgroundImage = 'none'; removeElementById('background-style'); removeElementById('background-mask'); } const color = accentColor || DEFAULT_CONFIG.accentColor; const style = getOrCreateStyleElement('dynamic-style'); style.textContent = ` .bookmark-item:hover, .bookmark-item.selected { border-left-color: ${color}; } .bookmark-item:hover { background-color: ${hexToRgba(color, STYLE_CONSTANTS.HOVER_OPACITY)}; } .bookmark-item.selected { background-color: ${hexToRgba(color, STYLE_CONSTANTS.SELECTED_OPACITY)}; } `; } function hexToRgba(hex, alpha) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } const commands = [ { name: ':list', description: 'Show all bookmarks' }, { name: ':config', description: 'Open settings' }, { name: ':bookmark', description: 'Edit bookmarks' }, { name: ':export', description: 'Export settings and bookmarks' }, { name: ':import', description: 'Import settings and bookmarks' }, { name: ':help', description: 'Show help' }, { name: ':reset', description: 'Reset all settings and bookmarks' } ]; let showingAllBookmarks = false; const commandHandlers = { ':config': () => { window.location.href = 'config.html'; }, ':bookmark': () => { window.location.href = 'bookmarks.html'; }, ':help': () => { toggleHelp(); }, ':list': () => { // Reload bookmarks from localStorage to get latest changes const saved = localStorage.getItem(STORAGE_KEYS.BOOKMARKS); if (saved) { bookmarks = JSON.parse(saved); } showingAllBookmarks = true; filteredBookmarks = bookmarks; selectedIndex = 0; renderResults(); searchInput.value = ''; searchInput.focus(); }, ':reset': () => { if (confirm('Reset all settings and bookmarks to default? This cannot be undone.')) { localStorage.removeItem(STORAGE_KEYS.CONFIG); localStorage.removeItem(STORAGE_KEYS.BOOKMARKS); window.location.reload(); } }, ':export': () => { exportData(); }, ':import': () => { importData(); } }; /** * Executes a command if the query matches a known command. * @param {string} query - The command string to execute * @returns {boolean} - True if command was executed, false otherwise */ function executeCommand(query) { const handler = commandHandlers[query]; if (handler) { handler(); return true; } return false; } function getFromLocalStorage(key, fallback) { const saved = localStorage.getItem(key); return saved ? JSON.parse(saved) : fallback; } /** * Exports settings and bookmarks as a JSON file */ function exportData() { const data = { config: getFromLocalStorage(STORAGE_KEYS.CONFIG, config), bookmarks: getFromLocalStorage(STORAGE_KEYS.BOOKMARKS, bookmarks), exportDate: new Date().toISOString() }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const today = new Date().toISOString().split('T')[0]; a.href = url; a.download = `startpage-export-${today}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * Imports settings and bookmarks from a JSON file */ function importData() { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const data = JSON.parse(event.target.result); if (data.config) { localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(data.config)); } if (data.bookmarks) { localStorage.setItem(STORAGE_KEYS.BOOKMARKS, JSON.stringify(data.bookmarks)); } alert('Import successful! Page will reload.'); window.location.reload(); } catch (error) { alert('Failed to import: Invalid file format'); console.error('Import error:', error); } }; reader.readAsText(file); }; input.click(); } /** * Checks if a bookmark matches the search query. * @param {Object} bookmark - The bookmark to check * @param {string} lowerQuery - The lowercased search query * @returns {boolean} - True if bookmark matches */ function bookmarkMatches(bookmark, lowerQuery) { return bookmark.name.toLowerCase().includes(lowerQuery) || bookmark.url.toLowerCase().includes(lowerQuery) || bookmark.tags.some(tag => tag.toLowerCase().includes(lowerQuery)); } /** * Filters bookmarks or shows command suggestions based on query. * @param {string} query - The search query */ function filterBookmarks(query) { if (!query) { filteredBookmarks = showingAllBookmarks ? bookmarks : []; currentCommandSuggestions = []; selectedIndex = 0; renderResults(); return; } if (query.startsWith(':')) { showingAllBookmarks = false; const matchedCommands = commands.filter(cmd => cmd.name.startsWith(query.toLowerCase()) ); currentCommandSuggestions = matchedCommands; selectedIndex = 0; renderCommandSuggestions(matchedCommands); return; } currentCommandSuggestions = []; const lowerQuery = query.toLowerCase(); filteredBookmarks = bookmarks.filter(bookmark => bookmarkMatches(bookmark, lowerQuery)); selectedIndex = 0; renderResults(); } function createTextElement(className, text) { const element = document.createElement('div'); element.className = className; element.textContent = text; return element; } function createResultItem(isSelected, onClickHandler) { const item = document.createElement('div'); item.className = 'bookmark-item' + (isSelected ? ' selected' : ''); item.addEventListener('click', onClickHandler); return item; } function renderCommandSuggestions(matchedCommands) { resultsContainer.innerHTML = ''; matchedCommands.forEach((cmd, index) => { const item = createResultItem(index === selectedIndex, () => { searchInput.value = cmd.name; executeCommand(cmd.name); }); item.appendChild(createTextElement('bookmark-name', cmd.name)); item.appendChild(createTextElement('bookmark-url', cmd.description)); resultsContainer.appendChild(item); }); updateScrollFade(); } function createTagsElement(tags) { const tagsContainer = document.createElement('div'); tagsContainer.className = 'bookmark-tags'; tags.forEach(tag => { const tagSpan = document.createElement('span'); tagSpan.className = 'tag'; tagSpan.textContent = `#${tag}`; tagsContainer.appendChild(tagSpan); }); return tagsContainer; } function renderResults() { resultsContainer.innerHTML = ''; filteredBookmarks.forEach((bookmark, index) => { const item = createResultItem(index === selectedIndex, () => openBookmark(bookmark)); item.appendChild(createTextElement('bookmark-name', bookmark.name)); item.appendChild(createTextElement('bookmark-url', bookmark.url)); item.appendChild(createTagsElement(bookmark.tags)); resultsContainer.appendChild(item); }); updateScrollFade(); } function openBookmark(bookmark) { window.location.href = bookmark.url; } function toggleHelp() { const existingOverlay = document.getElementById('help-overlay'); if (existingOverlay) { existingOverlay.remove(); return; } const accentColor = config.accentColor || DEFAULT_CONFIG.accentColor; const textColor = config.textColor || DEFAULT_CONFIG.textColor; const helpOverlay = document.createElement('div'); helpOverlay.id = 'help-overlay'; Object.assign(helpOverlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', background: 'rgba(0, 0, 0, 0.9)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: '1000' }); const helpContent = document.createElement('div'); Object.assign(helpContent.style, { background: 'rgba(20, 20, 20, 0.95)', border: `1px solid ${accentColor}`, borderRadius: '8px', padding: '40px', maxWidth: '500px', color: textColor }); helpContent.innerHTML = `