Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation
This commit is contained in:
285
LinkSyncExtension/popup.js
Normal file
285
LinkSyncExtension/popup.js
Normal file
@@ -0,0 +1,285 @@
|
||||
// LinkSync Popup Script
|
||||
// Handles bookmark management and sync operations
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
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]));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when page loads
|
||||
window.addEventListener('load', () => Popup.init());
|
||||
|
||||
// Expose to window
|
||||
window.Popup = Popup;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user