// LinkSync Popup Script // Handles bookmark management, sync, collections, and queries const Popup = { bookmarks: [], collections: [], async init() { this.setupTabs(); this.setupEventListeners(); await this.loadSettings(); await this.loadBookmarks(); await this.loadCollections(); this.updateSyncStatus(); }, setupTabs() { document.querySelectorAll(".tab").forEach((tab) => { tab.addEventListener("click", () => { document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active")); document.querySelectorAll(".tab-content").forEach((c) => c.classList.remove("active")); tab.classList.add("active"); document.getElementById(`tab-${tab.dataset.tab}`).classList.add("active"); }); }); }, setupEventListeners() { document.getElementById("add-bookmark-form").addEventListener("submit", (e) => { e.preventDefault(); this.addBookmark(); }); document.getElementById("search").addEventListener("input", (e) => { this.filterBookmarks(e.target.value); }); document.getElementById("sync-btn").addEventListener("click", () => this.syncNow()); document.getElementById("settings-btn").addEventListener("click", () => this.openSettings()); document.getElementById("close-settings").addEventListener("click", () => this.closeSettings()); document.getElementById("settings-form").addEventListener("submit", (e) => { e.preventDefault(); this.saveSettings(); }); document.getElementById("toggle-key").addEventListener("click", () => this.toggleApiKey()); document.getElementById("test-connection").addEventListener("click", () => this.testConnection()); document.getElementById("parse-btn").addEventListener("click", () => this.parseQuery()); document.getElementById("execute-btn").addEventListener("click", () => this.executeQuery()); document.getElementById("settings-modal").addEventListener("click", (e) => { if (e.target.id === "settings-modal") this.closeSettings(); }); }, async loadSettings() { const settings = await browser.storage.local.get([ "linksync_server_url", "linksync_api_key", "linksync_sync_mode", "linksync_deletions", "linksync_auto_sync", ]); document.getElementById("server-url").value = settings.linksync_server_url || "http://localhost:5000"; document.getElementById("api-key").value = settings.linksync_api_key || ""; document.getElementById("sync-mode").value = settings.linksync_sync_mode || "bi-directional"; document.getElementById("deletions").checked = settings.linksync_deletions === true; document.getElementById("auto-sync").checked = settings.linksync_auto_sync === true; }, async openSettings() { document.getElementById("settings-modal").classList.add("open"); }, closeSettings() { document.getElementById("settings-modal").classList.remove("open"); }, toggleApiKey() { const input = document.getElementById("api-key"); const btn = document.getElementById("toggle-key"); if (input.type === "password") { input.type = "text"; btn.textContent = "Hide"; } else { input.type = "password"; btn.textContent = "Show"; } }, async saveSettings() { const settings = { linksync_server_url: document.getElementById("server-url").value.replace(/\/+$/, ""), linksync_api_key: document.getElementById("api-key").value, linksync_sync_mode: document.getElementById("sync-mode").value, linksync_deletions: document.getElementById("deletions").checked, linksync_auto_sync: document.getElementById("auto-sync").checked, }; await browser.storage.local.set(settings); await API.init(); this.closeSettings(); this.notify("Settings saved", "success"); await this.loadBookmarks(); }, async testConnection() { try { const result = await browser.runtime.sendMessage({ type: "TEST_CONNECTION" }); if (result.success) { this.notify("Connection successful", "success"); } else { this.notify(`Connection failed: ${result.error}`, "error"); } } catch (e) { this.notify(`Connection failed: ${e.message}`, "error"); } }, async loadBookmarks() { const container = document.getElementById("bookmarks-container"); container.innerHTML = '

Loading bookmarks...

'; try { const response = await browser.runtime.sendMessage({ type: "GET_BOOKMARKS", data: { limit: 50 } }); this.bookmarks = response || []; this.renderBookmarks(this.bookmarks); } catch (e) { container.innerHTML = `

Error: ${e.message}

`; } }, renderBookmarks(bookmarks) { const container = document.getElementById("bookmarks-container"); if (!bookmarks || bookmarks.length === 0) { container.innerHTML = '

No bookmarks found

'; return; } container.innerHTML = bookmarks .map( (bm) => `
${this.escapeHtml(bm.url)}
${this.escapeHtml(bm.title)}
${bm.description ? `
${this.escapeHtml(bm.description)}
` : ""} ${bm.tags && bm.tags.length > 0 ? `
${bm.tags.map((t) => `${this.escapeHtml(t)}`).join("")}
` : ""}
` ) .join(""); }, filterBookmarks(query) { const q = query.toLowerCase(); const filtered = this.bookmarks.filter( (b) => (b.title && b.title.toLowerCase().includes(q)) || (b.url && b.url.toLowerCase().includes(q)) || (b.description && b.description.toLowerCase().includes(q)) || (b.tags && b.tags.some((t) => t.toLowerCase().includes(q))) ); this.renderBookmarks(filtered); }, async addBookmark() { const data = { url: document.getElementById("url").value, title: document.getElementById("title").value, description: document.getElementById("description").value, notes: document.getElementById("notes").value, tags: this.formatTags(document.getElementById("tags").value), path: document.getElementById("folder").value, }; try { const result = await browser.runtime.sendMessage({ type: "CREATE_BOOKMARK", data }); document.getElementById("add-bookmark-form").reset(); this.notify("Bookmark added", "success"); await this.loadBookmarks(); } catch (e) { this.notify(`Failed to add bookmark: ${e.message}`, "error"); } }, formatTags(tagString) { if (!tagString) return []; return tagString .split(",") .map((t) => t.trim()) .filter((t) => t.length > 0); }, async loadCollections() { const container = document.getElementById("collections-container"); container.innerHTML = '

Loading collections...

'; try { const response = await browser.runtime.sendMessage({ type: "GET_COLLECTIONS" }); this.collections = response || []; this.renderCollections(this.collections); } catch (e) { container.innerHTML = `

Error: ${e.message}

`; } }, renderCollections(collections) { const container = document.getElementById("collections-container"); if (!collections || collections.length === 0) { container.innerHTML = '

No collections

'; return; } container.innerHTML = collections .map( (c) => `

${this.escapeHtml(c.name)}

${c.description ? `

${this.escapeHtml(c.description)}

` : ""} ${c.query_type}
` ) .join(""); }, async syncNow() { const indicator = document.getElementById("sync-indicator"); indicator.className = "syncing"; try { const result = await browser.runtime.sendMessage({ type: "SYNC_NOW" }); if (result && result.success) { indicator.className = "synced"; this.notify(`Sync completed: ${result.actions?.length || 0} actions`, "success"); } else { indicator.className = "error"; this.notify(`Sync failed: ${result?.error || "Unknown error"}`, "error"); } } catch (e) { indicator.className = "error"; this.notify(`Sync error: ${e.message}`, "error"); } setTimeout(() => { if (indicator.className === "syncing") indicator.className = ""; }, 3000); this.updateSyncStatus(); }, updateSyncStatus() { browser.storage.local.get(["linksync_last_sync", "linksync_syncing"]).then((settings) => { const indicator = document.getElementById("sync-indicator"); const lastSync = document.getElementById("last-sync"); if (settings.linksync_syncing) { indicator.className = "syncing"; lastSync.textContent = "Syncing..."; return; } if (settings.linksync_last_sync) { const date = new Date(settings.linksync_last_sync); const mins = Math.floor((Date.now() - date.getTime()) / 60000); if (mins < 1) { indicator.className = "synced"; lastSync.textContent = "Just now"; } else if (mins < 60) { indicator.className = "synced"; lastSync.textContent = `${mins}m ago`; } else { indicator.className = ""; lastSync.textContent = date.toLocaleDateString(); } } }); }, async parseQuery() { const expression = document.getElementById("query-input").value.trim(); if (!expression) { this.notify("Enter a query expression", "info"); return; } try { const result = await browser.runtime.sendMessage({ type: "PARSE_QUERY", data: { expression } }); const output = document.getElementById("query-result"); if (result.valid) { output.innerHTML = `
${JSON.stringify(result.parsed, null, 2)}
`; this.notify("Query parsed successfully", "success"); } else { output.innerHTML = `Invalid: ${this.escapeHtml(result.error)}`; this.notify("Invalid query syntax", "error"); } } catch (e) { this.notify(`Parse error: ${e.message}`, "error"); } }, async executeQuery() { const expression = document.getElementById("query-input").value.trim(); if (!expression) { this.notify("Enter a query expression", "info"); return; } const output = document.getElementById("query-result"); output.innerHTML = '

Executing...

'; try { const results = await browser.runtime.sendMessage({ type: "EXECUTE_QUERY", data: { expression, limit: 50 }, }); const items = results || []; if (items.length === 0) { output.innerHTML = '

No results

'; } else { output.innerHTML = items .map( (bm) => `
${this.escapeHtml(bm.title || bm.url)}
` ) .join(""); this.notify(`Found ${items.length} results`, "success"); } } catch (e) { output.innerHTML = `Error: ${this.escapeHtml(e.message)}`; this.notify(`Query error: ${e.message}`, "error"); } }, notify(message, type = "info") { const container = document.getElementById("notification-container"); const el = document.createElement("div"); el.className = `notification ${type}`; el.textContent = message; container.appendChild(el); setTimeout(() => { el.style.opacity = "0"; el.style.transition = "opacity 0.3s"; setTimeout(() => el.remove(), 300); }, 3000); }, escapeHtml(str) { if (!str) return ""; const div = document.createElement("div"); div.textContent = str; return div.innerHTML; }, }; document.addEventListener("DOMContentLoaded", () => Popup.init());