let entries = JSON.parse(localStorage.getItem('bm_entries')) || []; let currentFilter = null; const GITHUB_EXAMPLES_URL = "https://raw.githubusercontent.com/mrhappynice/bookmarklets/refs/heads/main/entries.json"; document.addEventListener('DOMContentLoaded', () => { if (entries.length === 0) { fetch('entries.json') .then(res => res.json()) .then(data => { entries = data; saveAndRefresh(); }).catch(() => renderGrid()); } else { renderGrid(); renderTags(); } document.getElementById('search-input').addEventListener('input', (e) => { renderGrid(e.target.value); }); }); function saveAndRefresh() { localStorage.setItem('bm_entries', JSON.stringify(entries)); renderGrid(); renderTags(); } // --- FIX: Proper Attribute Escaping for 3D Page and others --- function escapeHtmlAttribute(str) { if (!str) return ""; return str.replace(/"/g, '"'); } function renderGrid(searchTerm = "") { const grid = document.getElementById('grid'); grid.innerHTML = ""; const filtered = entries.filter(item => { const matchesSearch = (item.title + item.description + (item.tags || []).join(' ')) .toLowerCase().includes(searchTerm.toLowerCase()); const matchesTag = !currentFilter || (item.tags && item.tags.includes(currentFilter)); return matchesSearch && matchesTag; }); filtered.forEach(item => { const card = document.createElement('div'); card.className = 'card'; const descHtml = (item.description || "").replace(/#(\w+)/g, '#$1'); // Use the escape function to prevent the code from breaking the href attribute const safeCode = escapeHtmlAttribute(item.code); card.innerHTML = `

${item.title}

${descHtml}

Drag Me
Drag to bookmarks bar
`; grid.appendChild(card); }); } function renderTags() { const bar = document.getElementById('tag-filter-bar'); const allTags = new Set(); entries.forEach(e => (e.tags || []).forEach(t => allTags.add(t))); bar.innerHTML = `
All
`; allTags.forEach(tag => { bar.innerHTML += `
#${tag}
`; }); } function filterByTag(tag) { currentFilter = (currentFilter === tag) ? null : tag; renderTags(); renderGrid(); } // --- MODAL & EDITOR --- function openNew() { showEditor({ id: crypto.randomUUID(), title: "", description: "", code: "javascript:", tags: [] }); } function editEntry(id) { const entry = entries.find(e => e.id === id); if (entry) showEditor(entry); } function showEditor(entry) { const modal = document.getElementById('modal'); const body = document.getElementById('wizard-body'); document.getElementById('save-btn').style.display = 'block'; body.innerHTML = `
`; modal.classList.add('open'); } function saveEntry() { const id = document.getElementById('edit-id').value; const title = document.getElementById('edit-title').value; const description = document.getElementById('edit-desc').value; const code = document.getElementById('edit-code').value; const tags = (description.match(/#(\w+)/g) || []).map(t => t.substring(1).toLowerCase()); const index = entries.findIndex(e => e.id === id); const newEntry = { id, title, description, code, tags, updated_at: new Date().toISOString() }; if (index > -1) entries[index] = newEntry; else entries.push(newEntry); saveAndRefresh(); closeModal(); } function closeModal() { document.getElementById('modal').classList.remove('open'); } function deleteEntry(id) { if (confirm("Delete this?")) { entries = entries.filter(e => e.id !== id); saveAndRefresh(); } } // --- DATA & URL IMPORT --- function downloadData() { const blob = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `bookmarklets_backup.json`; a.click(); } function triggerImport() { document.getElementById('import-file').click(); } function handleFileImport(input) { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { mergeEntries(JSON.parse(e.target.result)); } catch (err) { alert("Invalid JSON"); } }; reader.readAsText(file); } function openUrlModal() { const modal = document.getElementById('modal'); const body = document.getElementById('wizard-body'); document.getElementById('save-btn').style.display = 'none'; body.innerHTML = `

Load from URL

`; modal.classList.add('open'); } async function fetchRemoteData(url) { const targetUrl = url || document.getElementById('remote-url').value; if (!targetUrl) return alert("Please enter a URL."); try { const response = await fetch(targetUrl); const data = await response.json(); mergeEntries(data); closeModal(); } catch (err) { alert("Failed: " + err.message); } } function mergeEntries(newEntries) { if (!Array.isArray(newEntries)) return; let count = 0; newEntries.forEach(item => { if (!entries.find(e => e.id === item.id)) { entries.push(item); count++; } }); saveAndRefresh(); alert(`Added ${count} items.`); }