Complete LinkSyncServer and LinkSyncExtension implementation
LinkSyncServer: - Fix app.py imports, add CORS middleware, lifespan events - Create api/routes.py router aggregator - Create config/settings.py for centralized configuration - Rewrite models/base.py with proper relationships and serialization - Rewrite all API endpoints with real DB integration (auth, links, collections, sync, queries, tags) - Add admin endpoints (user management, stats, audit log) - Complete query parser with recursive descent and proper precedence - Complete query executor with set operations and field filters - Set up Alembic migrations with initial schema - Create web interface (templates, CSS, JS) - Add 42 passing tests (auth, links, collections, queries) - Add deploy.ps1 and deploy.sh scripts - Update README with deployment workflow LinkSyncExtension: - Create utils/api.js (REST client with retries, auth, error handling) - Create utils/sync.js (3 sync modes + conflict detection) - Create utils/collection.js (collection management) - Create utils/query-engine.js (client-side query parser) - Rewrite background.js (sync loop, bookmark events, message routing) - Rewrite popup.js (tabs, settings modal, notifications, CRUD) - Update popup.html (tabbed interface, query builder, modal) - Update popup.css (full redesign) - Create content/content.js (page metadata extraction) - Create options.html/js (dedicated settings page) - Generate icons (48x48, 96x96) - Update manifest.json (host permissions, content scripts, options) - Create AGENTS.md
This commit is contained in:
@@ -1,285 +1,365 @@
|
||||
// LinkSync Popup Script
|
||||
// Handles bookmark management and sync operations
|
||||
// Handles bookmark management, sync, collections, and queries
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
const Popup = {
|
||||
bookmarks: [],
|
||||
collections: [],
|
||||
|
||||
const Popup = {
|
||||
// API Configuration
|
||||
API_BASE_URL: '',
|
||||
API_KEY: '',
|
||||
|
||||
// Initialize popup
|
||||
async init() {
|
||||
console.log('LinkSync: Popup initialized');
|
||||
|
||||
// Load settings
|
||||
await this.loadSettings();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Load bookmarks
|
||||
await this.loadBookmarks();
|
||||
|
||||
// Load collections
|
||||
await this.loadCollections();
|
||||
|
||||
// Update sync status
|
||||
this.updateSyncStatus();
|
||||
},
|
||||
|
||||
// Load settings from storage
|
||||
async loadSettings() {
|
||||
this.API_BASE_URL = await this.getSetting('url') || 'http://localhost:5000';
|
||||
this.API_KEY = await this.getSetting('apiKey') || '';
|
||||
|
||||
// Update form
|
||||
this.updateFormState();
|
||||
},
|
||||
|
||||
// Get setting from storage
|
||||
async getSetting(key) {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.get(key, result => resolve(result[key]));
|
||||
});
|
||||
},
|
||||
|
||||
// Setup event listeners
|
||||
setupEventListeners() {
|
||||
// Form submission
|
||||
document.getElementById('bookmark-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.addBookmark();
|
||||
});
|
||||
|
||||
// Search filter
|
||||
document.getElementById('search').addEventListener('input', async (e) => {
|
||||
await this.filterBookmarks(e.target.value);
|
||||
});
|
||||
|
||||
// Sync button
|
||||
document.getElementById('sync-btn').addEventListener('click', async () => {
|
||||
await this.syncBookmarks();
|
||||
});
|
||||
|
||||
// Settings button
|
||||
document.getElementById('settings-btn').addEventListener('click', () => {
|
||||
this.openSettings();
|
||||
});
|
||||
},
|
||||
|
||||
// Update form state (edit mode)
|
||||
updateFormState(isEdit = false) {
|
||||
const form = document.getElementById('bookmark-form');
|
||||
if (isEdit) {
|
||||
form.style.display = 'block';
|
||||
} else {
|
||||
form.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
// Load bookmarks from server
|
||||
async loadBookmarks() {
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}/api/links/`, {
|
||||
headers: { 'Authorization': `Token ${this.API_KEY}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.renderBookmarks(data.links || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Failed to load bookmarks:', error);
|
||||
this.renderError('Unable to connect to server. Check your settings.');
|
||||
}
|
||||
},
|
||||
|
||||
// Render bookmarks to list
|
||||
renderBookmarks(bookmarks) {
|
||||
const container = document.getElementById('bookmarks-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!bookmarks || bookmarks.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--secondary); font-size: 12px;">No bookmarks</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
bookmarks.forEach(bookmark => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'bookmark-item';
|
||||
item.innerHTML = `
|
||||
<a href="${bookmark.url}" target="_blank">${bookmark.url}</a>
|
||||
<div class="title">${bookmark.title}</div>
|
||||
${bookmark.description ? `<div class="description">${bookmark.description}</div>` : ''}
|
||||
${bookmark.tags && bookmark.tags.length > 0 ? `<div class="tags">${bookmark.tags.join(', ')}</div>` : ''}
|
||||
`;
|
||||
container.appendChild(item);
|
||||
});
|
||||
},
|
||||
|
||||
// Filter bookmarks by search term
|
||||
async filterBookmarks(query) {
|
||||
const bookmarks = await this.loadBookmarks();
|
||||
const filtered = bookmarks.filter(b =>
|
||||
b.title.toLowerCase().includes(query.toLowerCase()) ||
|
||||
b.url.toLowerCase().includes(query.toLowerCase()) ||
|
||||
(b.description && b.description.toLowerCase().includes(query.toLowerCase()))
|
||||
);
|
||||
this.renderBookmarks(filtered);
|
||||
},
|
||||
|
||||
// Add bookmark
|
||||
async addBookmark() {
|
||||
const form = document.getElementById('bookmark-form');
|
||||
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
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.API_BASE_URL}/api/links/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Token ${this.API_KEY}`
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
form.reset();
|
||||
await this.loadBookmarks();
|
||||
this.showNotification('Bookmark added', 'success');
|
||||
} else {
|
||||
this.showNotification('Failed to add bookmark', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// Format tags
|
||||
formatTags(tagString) {
|
||||
if (!tagString) return [];
|
||||
return tagString.split(',').map(t => t.trim()).filter(t => t.length > 0);
|
||||
},
|
||||
|
||||
// Load collections
|
||||
async loadCollections() {
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}/api/collections/`, {
|
||||
headers: { 'Authorization': `Token ${this.API_KEY}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.renderCollections(data.collections || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Failed to load collections:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Render collections
|
||||
renderCollections(collections) {
|
||||
const container = document.getElementById('collections-list');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!collections || collections.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--secondary); font-size: 12px;">No collections</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
collections.forEach(collection => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'collection-item';
|
||||
item.innerHTML = `
|
||||
<h3>${collection.name}</h3>
|
||||
<p>${collection.description || ''}</p>
|
||||
<p style="font-size: 10px; color: var(--secondary);">Type: ${collection.query_type || 'dynamic'}</p>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
});
|
||||
},
|
||||
|
||||
// Sync bookmarks
|
||||
async syncBookmarks() {
|
||||
const indicator = document.getElementById('sync-indicator');
|
||||
indicator.className = 'syncing';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}/api/sync/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Token ${this.API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bookmarks: [],
|
||||
mode: await this.getSetting('mode') || 'bi-directional',
|
||||
deletions: await this.getSetting('deletions') || false
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
indicator.className = 'synced';
|
||||
document.getElementById('last-sync').textContent = `Last sync: ${new Date().toLocaleTimeString()}`;
|
||||
this.showNotification('Sync completed', 'success');
|
||||
} else {
|
||||
this.showNotification('Sync failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Sync error:', error);
|
||||
this.showNotification('Sync error', 'error');
|
||||
} finally {
|
||||
setTimeout(() => indicator.className = '', 2000);
|
||||
}
|
||||
},
|
||||
|
||||
// Update sync status
|
||||
updateSyncStatus() {
|
||||
const indicator = document.getElementById('sync-indicator');
|
||||
const lastSync = document.getElementById('last-sync');
|
||||
|
||||
const lastSyncTime = new Date(await this.getSetting('lastSync') || Date.now());
|
||||
const minutesAgo = Math.floor((Date.now() - lastSyncTime.getTime()) / 60000);
|
||||
|
||||
if (minutesAgo < 5) {
|
||||
indicator.className = 'synced';
|
||||
lastSync.textContent = `Synced ${minutesAgo} min ago`;
|
||||
} else {
|
||||
indicator.className = 'error';
|
||||
lastSync.textContent = `Last sync: ${lastSyncTime.toLocaleString()}`;
|
||||
}
|
||||
},
|
||||
|
||||
// Open settings modal
|
||||
openSettings() {
|
||||
// TODO: Open settings modal
|
||||
console.log('Open settings');
|
||||
},
|
||||
|
||||
// Show notification
|
||||
showNotification(message, type) {
|
||||
// TODO: Show toast notification
|
||||
console.log(`[LinkSync] ${message}`);
|
||||
},
|
||||
|
||||
// Get setting
|
||||
async getSetting(key) {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.get(key, result => resolve(result[key]));
|
||||
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";
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when page loads
|
||||
window.addEventListener('load', () => Popup.init());
|
||||
|
||||
// Expose to window
|
||||
window.Popup = Popup;
|
||||
|
||||
})();
|
||||
},
|
||||
|
||||
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 = '<p class="empty-state">Loading bookmarks...</p>';
|
||||
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({ type: "GET_BOOKMARKS", data: { limit: 50 } });
|
||||
this.bookmarks = response || [];
|
||||
this.renderBookmarks(this.bookmarks);
|
||||
} catch (e) {
|
||||
container.innerHTML = `<p class="empty-state">Error: ${e.message}</p>`;
|
||||
}
|
||||
},
|
||||
|
||||
renderBookmarks(bookmarks) {
|
||||
const container = document.getElementById("bookmarks-container");
|
||||
|
||||
if (!bookmarks || bookmarks.length === 0) {
|
||||
container.innerHTML = '<p class="empty-state">No bookmarks found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = bookmarks
|
||||
.map(
|
||||
(bm) => `
|
||||
<div class="bookmark-item" data-id="${bm.id}">
|
||||
<a href="${this.escapeHtml(bm.url)}" target="_blank">${this.escapeHtml(bm.url)}</a>
|
||||
<div class="bm-title">${this.escapeHtml(bm.title)}</div>
|
||||
${bm.description ? `<div class="bm-desc">${this.escapeHtml(bm.description)}</div>` : ""}
|
||||
${bm.tags && bm.tags.length > 0
|
||||
? `<div class="bm-tags">${bm.tags.map((t) => `<span class="bm-tag">${this.escapeHtml(t)}</span>`).join("")}</div>`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.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 = '<p class="empty-state">Loading collections...</p>';
|
||||
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({ type: "GET_COLLECTIONS" });
|
||||
this.collections = response || [];
|
||||
this.renderCollections(this.collections);
|
||||
} catch (e) {
|
||||
container.innerHTML = `<p class="empty-state">Error: ${e.message}</p>`;
|
||||
}
|
||||
},
|
||||
|
||||
renderCollections(collections) {
|
||||
const container = document.getElementById("collections-container");
|
||||
|
||||
if (!collections || collections.length === 0) {
|
||||
container.innerHTML = '<p class="empty-state">No collections</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = collections
|
||||
.map(
|
||||
(c) => `
|
||||
<div class="collection-item">
|
||||
<h3>${this.escapeHtml(c.name)}</h3>
|
||||
${c.description ? `<p>${this.escapeHtml(c.description)}</p>` : ""}
|
||||
<span class="col-type">${c.query_type}</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.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 = `<pre style="white-space:pre-wrap;font-size:10px;">${JSON.stringify(result.parsed, null, 2)}</pre>`;
|
||||
this.notify("Query parsed successfully", "success");
|
||||
} else {
|
||||
output.innerHTML = `<span style="color:var(--error)">Invalid: ${this.escapeHtml(result.error)}</span>`;
|
||||
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 = '<p class="empty-state">Executing...</p>';
|
||||
|
||||
try {
|
||||
const results = await browser.runtime.sendMessage({
|
||||
type: "EXECUTE_QUERY",
|
||||
data: { expression, limit: 50 },
|
||||
});
|
||||
const items = results || [];
|
||||
if (items.length === 0) {
|
||||
output.innerHTML = '<p class="empty-state">No results</p>';
|
||||
} else {
|
||||
output.innerHTML = items
|
||||
.map(
|
||||
(bm) => `
|
||||
<div class="bookmark-item" style="margin-bottom:4px;">
|
||||
<a href="${this.escapeHtml(bm.url)}" target="_blank">${this.escapeHtml(bm.title || bm.url)}</a>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
this.notify(`Found ${items.length} results`, "success");
|
||||
}
|
||||
} catch (e) {
|
||||
output.innerHTML = `<span style="color:var(--error)">Error: ${this.escapeHtml(e.message)}</span>`;
|
||||
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());
|
||||
|
||||
Reference in New Issue
Block a user