Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation

This commit is contained in:
DavidSaylor
2026-05-11 17:37:10 -05:00
parent ad0b12b452
commit aed69afdfd
691 changed files with 181874 additions and 28 deletions

285
LinkSyncExtension/popup.js Normal file
View 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;
})();