929 lines
24 KiB
JavaScript
929 lines
24 KiB
JavaScript
// LinkdingSync Popup JavaScript
|
||
// Handles popup UI, state management, and bookmark operations
|
||
|
||
'use strict';
|
||
|
||
// Direct browser.storage.local API for popup (no browser API prefix needed)
|
||
const storage = {
|
||
get(keys) {
|
||
return browser.storage.local.get(keys);
|
||
},
|
||
|
||
set(items) {
|
||
return browser.storage.local.set(items);
|
||
},
|
||
|
||
clear() {
|
||
return browser.storage.local.clear();
|
||
}
|
||
};
|
||
|
||
// Direct browser.bookmarks API for popup
|
||
const bookmarks = {
|
||
getTree() {
|
||
return browser.bookmarks.getTree();
|
||
},
|
||
|
||
get(id) {
|
||
return browser.bookmarks.get(id);
|
||
},
|
||
|
||
onCreated(handler) {
|
||
browser.bookmarks.onCreated.addListener(handler);
|
||
},
|
||
|
||
onChanged(handler) {
|
||
browser.bookmarks.onChanged.addListener(handler);
|
||
},
|
||
|
||
onRemoved(handler) {
|
||
browser.bookmarks.onRemoved.addListener(handler);
|
||
}
|
||
};
|
||
|
||
// Global API Client module (accessible from other scripts)
|
||
class ApiClient {
|
||
constructor(serverUrl, apiKey) {
|
||
this.serverUrl = serverUrl.endsWith('/') ? serverUrl : serverUrl + '/';
|
||
this.apiToken = apiKey;
|
||
this.baseURL = this.serverUrl + 'api';
|
||
}
|
||
|
||
async getAuthHeaders() {
|
||
return {
|
||
'Authorization': `Token ${this.apiToken}`
|
||
};
|
||
}
|
||
|
||
async getBundles() {
|
||
try {
|
||
const response = await fetch(`${this.baseURL}bundles/?limit=100`, {
|
||
headers: await this.getAuthHeaders()
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('Get bundles error:', error);
|
||
return { results: [], count: 0 };
|
||
}
|
||
}
|
||
|
||
async getBundle(bundleTag) {
|
||
try {
|
||
// Query bookmarks with this tag
|
||
const response = await fetch(`${this.baseURL}bookmarks/?limit=1&any=${encodeURIComponent(bundleTag)}`, {
|
||
headers: await this.getAuthHeaders()
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
return data;
|
||
} catch (error) {
|
||
return { results: [], count: 0 };
|
||
}
|
||
}
|
||
|
||
async getBundleDetails(bundleId) {
|
||
try {
|
||
const response = await fetch(`${this.baseURL}bundles/${bundleId}/`, {
|
||
headers: await this.getAuthHeaders()
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async checkUrl(url) {
|
||
try {
|
||
const response = await fetch(`${this.baseURL}bookmarks/check/?url=${encodeURIComponent(url)}`, {
|
||
headers: await this.getAuthHeaders()
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
return { bookmark: null };
|
||
}
|
||
}
|
||
|
||
async createBookmark(bookmarkData) {
|
||
try {
|
||
const response = await fetch(`${this.baseURL}bookmarks/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...await this.getAuthHeaders()
|
||
},
|
||
body: JSON.stringify(bookmarkData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.text();
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${error}`);
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('Create bookmark error:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async updateBookmark(bookmarkId, bookmarkData) {
|
||
try {
|
||
const response = await fetch(`${this.baseURL}bookmarks/${bookmarkId}/`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...await this.getAuthHeaders()
|
||
},
|
||
body: JSON.stringify(bookmarkData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.text();
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${error}`);
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('Update bookmark error:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async deleteBookmark(bookmarkId) {
|
||
try {
|
||
const response = await fetch(`${this.baseURL}bookmarks/${bookmarkId}/`, {
|
||
method: 'DELETE',
|
||
headers: await this.getAuthHeaders()
|
||
});
|
||
|
||
if (!response.ok && response.status !== 404) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Delete bookmark error:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async addBundle(bundleData) {
|
||
try {
|
||
const response = await fetch(`${this.baseURL}bundles/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...await this.getAuthHeaders()
|
||
},
|
||
body: JSON.stringify(bundleData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('Add bundle error:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
};
|
||
|
||
// Global Store for state management (uses storage object)
|
||
class Store {
|
||
async get(keys) {
|
||
try {
|
||
const data = await storage.get(keys);
|
||
return data;
|
||
} catch (error) {
|
||
console.error('Storage load error:', error);
|
||
return {};
|
||
}
|
||
}
|
||
|
||
async set(items) {
|
||
try {
|
||
await storage.set(items);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Storage save error:', error);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Global utility functions
|
||
const Utils = {
|
||
formatTimestamp(timestamp) {
|
||
if (!timestamp) return 'Never';
|
||
|
||
const date = new Date(timestamp);
|
||
const options = {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
timeZoneName: 'short'
|
||
};
|
||
|
||
return date.toLocaleDateString('en-US', options);
|
||
},
|
||
|
||
getBookmarkPath(bookmark) {
|
||
if (!bookmark.notes || typeof bookmark.notes !== 'string') {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
const notes = JSON.parse(bookmark.notes);
|
||
return notes.path || '';
|
||
} catch (e) {
|
||
return '';
|
||
}
|
||
},
|
||
|
||
setBookmarkPath(bookmark, path, userNotes) {
|
||
const notes = {
|
||
path: path,
|
||
userNotes: userNotes || '',
|
||
autoTags: []
|
||
};
|
||
|
||
bookmark.notes = JSON.stringify(notes);
|
||
},
|
||
|
||
extractTagsFromPath(path) {
|
||
if (!path) return [];
|
||
|
||
const folders = path.split('/').filter(f => f);
|
||
return folders.map(folder => ({
|
||
name: folder.trim()
|
||
}));
|
||
},
|
||
|
||
showMessage(message, type = 'info') {
|
||
const types = {
|
||
info: { color: '#2196f3', icon: 'ℹ️' },
|
||
success: { color: '#4caf50', icon: '✓' },
|
||
warning: { color: '#ff9800', icon: '⚠️' },
|
||
error: { color: '#f44336', icon: '✕' }
|
||
};
|
||
|
||
const style = types[type] || types.info;
|
||
|
||
// Remove existing notifications
|
||
const existing = document.getElementById('notification-area');
|
||
if (existing) existing.remove();
|
||
|
||
// Create notification
|
||
const notification = document.createElement('div');
|
||
notification.id = 'notification-area';
|
||
notification.style.cssText = `
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: ${style.color};
|
||
color: white;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||
z-index: 9999;
|
||
font-size: 13px;
|
||
max-width: 90vw;
|
||
text-align: center;
|
||
`;
|
||
notification.textContent = `${style.icon} ${message}`;
|
||
document.body.appendChild(notification);
|
||
|
||
// Auto-remove after 4 seconds
|
||
setTimeout(() => {
|
||
if (notification.parentNode) {
|
||
notification.remove();
|
||
}
|
||
}, 4000);
|
||
}
|
||
};
|
||
|
||
// Global Notes Manager for version handling
|
||
class NotesManager {
|
||
static getHostname() {
|
||
try {
|
||
return window.location.hostname || navigator.deviceMemory || 'unknown';
|
||
} catch (e) {
|
||
return 'unknown';
|
||
}
|
||
}
|
||
|
||
static getProfileName() {
|
||
try {
|
||
return navigator.userAgent.toLowerCase().match(/(firefox|chrome|edge|safari|opera)/i)?.[1] || 'browser';
|
||
} catch (e) {
|
||
return 'browser';
|
||
}
|
||
}
|
||
|
||
static generateBundleTag(serverUrl) {
|
||
const hostname = this.getHostname();
|
||
const profile = this.getProfileName();
|
||
const normalizedHost = hostname.replace(/\./g, '_').toLowerCase();
|
||
|
||
// If bundleTag is configured, use it
|
||
if (serverUrl && serverUrl.bundleTag) {
|
||
return serverUrl.bundleTag;
|
||
}
|
||
|
||
// Otherwise auto-generate from hostname/profile
|
||
return `bundle_${normalizedHost}_${profile}_${Date.now().toString().slice(-4)}`;
|
||
}
|
||
|
||
static parseVersion(version) {
|
||
const [major, minor] = version.split('.').map(Number);
|
||
return { major, minor };
|
||
}
|
||
|
||
static parseNotes(noteString) {
|
||
if (!noteString) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(noteString);
|
||
|
||
// Handle non-JSON notes (old bookmarks)
|
||
if (typeof parsed === 'string') {
|
||
return {
|
||
version: '1.0',
|
||
path: '',
|
||
userNotes: parsed,
|
||
autoTags: [],
|
||
bundleTag: null,
|
||
keyword: ''
|
||
};
|
||
}
|
||
|
||
// Handle notes without version field (old structured format)
|
||
if (!parsed.version) {
|
||
return {
|
||
version: '1.0',
|
||
path: parsed.path || '',
|
||
userNotes: parsed.userNotes || parsed.notes || parsed.description || '',
|
||
autoTags: parsed.autoTags || [],
|
||
bundleTag: parsed.bundleTag || null,
|
||
keyword: parsed.keyword || ''
|
||
};
|
||
}
|
||
|
||
// Handle version 1.x - basic fields only
|
||
if (this.parseVersion(parsed.version).major < 2) {
|
||
return {
|
||
version: parsed.version,
|
||
path: parsed.path || '',
|
||
userNotes: parsed.userNotes || '',
|
||
autoTags: parsed.autoTags || [],
|
||
bundleTag: parsed.bundleTag || null,
|
||
keyword: parsed.keyword || ''
|
||
};
|
||
}
|
||
|
||
// Handle version 2.x and above
|
||
return {
|
||
version: parsed.version,
|
||
path: parsed.path || '',
|
||
userNotes: parsed.userNotes || '',
|
||
autoTags: parsed.autoTags || [],
|
||
bundleTag: parsed.bundleTag || null,
|
||
keyword: parsed.keyword || '',
|
||
...parsed
|
||
};
|
||
} catch (e) {
|
||
// Invalid JSON - treat as old notes
|
||
return {
|
||
version: '1.0',
|
||
path: '',
|
||
userNotes: noteString,
|
||
autoTags: [],
|
||
bundleTag: null,
|
||
keyword: ''
|
||
};
|
||
}
|
||
}
|
||
|
||
static formatNotes(notesObject) {
|
||
if (!notesObject) {
|
||
return '';
|
||
}
|
||
|
||
return JSON.stringify(notesObject);
|
||
}
|
||
|
||
static createEmptyNotes(bundleTag) {
|
||
return {
|
||
version: '1.0',
|
||
path: '',
|
||
userNotes: '',
|
||
autoTags: [],
|
||
bundleTag: bundleTag,
|
||
keyword: ''
|
||
};
|
||
}
|
||
|
||
static shouldMigrate(notes) {
|
||
// Should migrate if:
|
||
// - notes is not a string (not JSON)
|
||
// - notes doesn't have version field
|
||
// - version is old (< 1.0)
|
||
if (!notes) {
|
||
return true;
|
||
}
|
||
|
||
if (typeof notes !== 'object') {
|
||
return true;
|
||
}
|
||
|
||
const version = notes.version || '0.0';
|
||
const current = this.parseVersion(version);
|
||
const target = this.parseVersion('1.0');
|
||
|
||
return current.major < target.major;
|
||
}
|
||
|
||
static migrateNotes(oldNotes, bundleTag) {
|
||
return {
|
||
version: '1.0',
|
||
path: '',
|
||
userNotes: typeof oldNotes === 'string' ? oldNotes : '',
|
||
autoTags: [],
|
||
bundleTag: bundleTag,
|
||
keyword: ''
|
||
};
|
||
}
|
||
|
||
static updateBundleTag(notes, bundleTag) {
|
||
if (!notes) return notes;
|
||
if (typeof notes !== 'object') return notes;
|
||
|
||
const parsed = this.parseNotes(notes);
|
||
|
||
// If this is our extension's notes (version 1.0 without bundleTag), update bundleTag
|
||
if (!parsed.bundleTag) {
|
||
return {
|
||
...parsed,
|
||
bundleTag: bundleTag
|
||
};
|
||
}
|
||
|
||
return notes;
|
||
}
|
||
};
|
||
|
||
// Global Sync Manager (uses storage directly for popup)
|
||
class SyncManager {
|
||
constructor() {
|
||
this.api = null;
|
||
this.store = new Store();
|
||
this.config = null;
|
||
this.lastSyncTime = null;
|
||
this.bundleTag = null;
|
||
this.bundleId = null;
|
||
}
|
||
|
||
async init(config) {
|
||
this.config = config;
|
||
this.api = new ApiClient(config.serverUrl, config.apiKey);
|
||
|
||
// Auto-generate bundle tag if not set
|
||
if (!config.bundleTag) {
|
||
this.bundleTag = NotesManager.generateBundleTag(config.serverUrl);
|
||
await this.store.set({ bundleTag: this.bundleTag });
|
||
} else {
|
||
this.bundleTag = config.bundleTag;
|
||
}
|
||
|
||
this.lastSyncTime = Date.now();
|
||
await this.store.set({ lastSyncTime: this.lastSyncTime });
|
||
|
||
// Ensure bundle exists
|
||
await this.ensureBundle();
|
||
}
|
||
|
||
async getBundle() {
|
||
const config = await this.store.get();
|
||
return config.bundleTag || this.bundleTag || null;
|
||
}
|
||
|
||
async ensureBundle() {
|
||
try {
|
||
// Get existing bundle
|
||
let bundleData = await this.getBundleDetails(this.bundleId);
|
||
|
||
if (!bundleData) {
|
||
// Bundle doesn't exist, create it
|
||
bundleData = await this.createBundle(this.bundleTag);
|
||
}
|
||
|
||
// Save bundle info
|
||
await this.store.set({
|
||
bundleId: bundleData.id,
|
||
bundleName: bundleData.name || this.bundleTag,
|
||
bundleTags: bundleData.all_tags || ''
|
||
});
|
||
|
||
return bundleData;
|
||
} catch (error) {
|
||
console.error('Ensure bundle error:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async createBundle(bundleTag) {
|
||
// Create new bundle with required_tags set to our bundle tag
|
||
const bundleData = {
|
||
name: bundleTag,
|
||
search: '',
|
||
any_tags: '',
|
||
all_tags: bundleTag,
|
||
excluded_tags: ''
|
||
};
|
||
|
||
return await this.api.addBundle(bundleData);
|
||
}
|
||
|
||
async checkBookmark(url) {
|
||
if (!url) return null;
|
||
|
||
try {
|
||
const result = await this.api.checkUrl(url);
|
||
return result.bookmark;
|
||
} catch (error) {
|
||
console.error('Check bookmark error:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async syncBookmark(bookmarkData, folder) {
|
||
// Get bundle tag from config or auto-generated
|
||
const config = await this.store.get();
|
||
const bundleTag = config.bundleTag || this.bundleTag;
|
||
|
||
if (!bundleTag) {
|
||
throw new Error('No bundle tag configured');
|
||
}
|
||
|
||
// Check if bookmark exists
|
||
const existing = await this.checkBookmark(bookmarkData.url);
|
||
|
||
if (existing) {
|
||
// Bookmark exists - update it
|
||
const existingNotes = this.parseAndMigrateNotes(existing.notes, bundleTag);
|
||
|
||
// Get autoGenerateTags setting
|
||
const autoGenerateTags = config.autoGenerateTags || false;
|
||
|
||
// Merge Firefox tags to Linkding tags (only if autoGenerateTags is enabled)
|
||
const mergedNotes = this.mergeNotes(existingNotes, bookmarkData.notes, bookmarkData.tags, autoGenerateTags);
|
||
|
||
// Update bookmark
|
||
const updated = await this.api.updateBookmark(existing.id, {
|
||
title: bookmarkData.title || existing.title,
|
||
description: bookmarkData.description || existing.description,
|
||
notes: mergedNotes,
|
||
// Use browser's keyword for consistent cross-browser sync
|
||
keyword: bookmarkData.keyword || existing.keyword || ''
|
||
});
|
||
|
||
return updated;
|
||
} else {
|
||
// Create new bookmark with bundle tag
|
||
const newNotes = NotesManager.createEmptyNotes(bundleTag);
|
||
|
||
// Add folder path if provided
|
||
if (folder) {
|
||
newNotes.path = folder;
|
||
}
|
||
|
||
// Add user notes if provided
|
||
if (bookmarkData.notes) {
|
||
newNotes.userNotes = bookmarkData.notes;
|
||
}
|
||
|
||
// Add Firefox tags if provided AND autoGenerateTags is enabled
|
||
if (bookmarkData.tags && autoGenerateTags) {
|
||
newNotes.autoTags = bookmarkData.tags;
|
||
}
|
||
|
||
// Add keyword if provided
|
||
if (bookmarkData.keyword) {
|
||
newNotes.keyword = bookmarkData.keyword;
|
||
}
|
||
|
||
// Create new bookmark
|
||
const newBookmark = {
|
||
url: bookmarkData.url,
|
||
title: bookmarkData.title,
|
||
description: bookmarkData.description,
|
||
notes: NotesManager.formatNotes(newNotes),
|
||
unread: false,
|
||
shared: false
|
||
};
|
||
|
||
return await this.api.createBookmark(newBookmark);
|
||
}
|
||
}
|
||
|
||
parseAndMigrateNotes(noteString, bundleTag) {
|
||
if (!noteString) {
|
||
return NotesManager.createEmptyNotes(bundleTag);
|
||
}
|
||
|
||
const parsed = NotesManager.parseNotes(noteString);
|
||
|
||
// Migrate if needed
|
||
if (NotesManager.shouldMigrate(parsed)) {
|
||
parsed = NotesManager.migrateNotes(parsed, bundleTag);
|
||
}
|
||
|
||
// Update bundle tag if this is our notes
|
||
parsed = NotesManager.updateBundleTag(parsed, bundleTag);
|
||
|
||
return parsed;
|
||
}
|
||
|
||
mergeNotes(notes1, notes2, tags = [], autoGenerateTags = false) {
|
||
// Parse both notes
|
||
const p1 = NotesManager.parseNotes(notes1);
|
||
const p2 = NotesManager.parseNotes(notes2);
|
||
|
||
// Combine tags (use Firefox tags if provided and autoGenerateTags is enabled)
|
||
const combinedTags = autoGenerateTags && tags.length > 0 ? tags : (p2.autoTags || []);
|
||
|
||
// Merge notes with Firefox keyword support
|
||
return {
|
||
version: Math.max(p1.version, p2.version),
|
||
path: p1.path || p2.path || '',
|
||
userNotes: p1.userNotes || p2.userNotes || '',
|
||
autoTags: combinedTags,
|
||
bundleTag: p1.bundleTag || p2.bundleTag,
|
||
// Store Firefox keyword in notes for cross-browser sync
|
||
keyword: p1.keyword || p2.keyword || ''
|
||
};
|
||
}
|
||
|
||
async deleteBookmark(url) {
|
||
const existing = await this.checkBookmark(url);
|
||
|
||
if (!existing) {
|
||
return { success: false, error: 'Bookmark not found' };
|
||
}
|
||
|
||
try {
|
||
await this.api.deleteBookmark(existing.id);
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('Delete bookmark error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
async getBundleInfo() {
|
||
const config = await this.store.get();
|
||
|
||
if (!config.apiKey || !config.serverUrl) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const bundleInfo = await this.ensureBundle();
|
||
|
||
if (!bundleInfo) {
|
||
return null;
|
||
}
|
||
|
||
// Get bookmarks with this tag using "all" query param
|
||
const response = await fetch(`${config.serverUrl}api/bookmarks/?limit=1000&all=${encodeURIComponent(bundleInfo.all_tags || '')}`, {
|
||
headers: await this.api.getAuthHeaders()
|
||
});
|
||
|
||
if (response.ok) {
|
||
const results = await response.json();
|
||
return {
|
||
bundleId: bundleInfo.id,
|
||
bundleTag: bundleInfo.name || this.bundleTag,
|
||
count: results.count || results.results?.length || 0,
|
||
results: results.results || []
|
||
};
|
||
}
|
||
|
||
return {
|
||
bundleId: bundleInfo.id,
|
||
bundleTag: bundleInfo.name || this.bundleTag,
|
||
count: 0,
|
||
results: []
|
||
};
|
||
} catch (error) {
|
||
console.error('Get bundle info error:', error);
|
||
return null;
|
||
}
|
||
}
|
||
};
|
||
|
||
// Export for console access
|
||
window.LinkdingSync = {
|
||
ApiClient: ApiClient,
|
||
Store: Store,
|
||
Utils: Utils,
|
||
NotesManager: NotesManager,
|
||
Manager: SyncManager
|
||
};
|
||
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
// Load configuration
|
||
try {
|
||
const config = await new Store().get();
|
||
|
||
// Auto-generate bundle tag if not set
|
||
if (!config.bundleTag && config.serverUrl) {
|
||
const bundleTag = NotesManager.generateBundleTag(config.serverUrl);
|
||
await new Store().set({ bundleTag: bundleTag });
|
||
config.bundleTag = bundleTag;
|
||
}
|
||
|
||
// Initialize sync manager
|
||
await new SyncManager().init(config);
|
||
|
||
// Populate status
|
||
updateStatus();
|
||
|
||
// Reset input fields
|
||
const urlInput = document.getElementById('bookmark-url');
|
||
if (urlInput) urlInput.value = '';
|
||
|
||
const notesInput = document.getElementById('bookmark-notes');
|
||
if (notesInput) notesInput.value = '';
|
||
|
||
const folderSelect = document.getElementById('bookmark-folder');
|
||
if (folderSelect) folderSelect.value = '';
|
||
|
||
} catch (error) {
|
||
console.error('Popup initialization error:', error);
|
||
Utils.showMessage('Error loading configuration', 'error');
|
||
}
|
||
|
||
// Event handlers
|
||
const btnAddBookmark = document.getElementById('btn-add-bookmark');
|
||
if (btnAddBookmark) {
|
||
btnAddBookmark.addEventListener('click', async () => {
|
||
const url = document.getElementById('bookmark-url').value.trim();
|
||
const notes = document.getElementById('bookmark-notes').value.trim();
|
||
const folder = document.getElementById('bookmark-folder').value;
|
||
|
||
if (!url) {
|
||
Utils.showMessage('Please enter a URL', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const syncManager = new SyncManager();
|
||
await syncManager.init(await new Store().get());
|
||
const currentUrl = window.location.href;
|
||
const existingBookmark = await syncManager.checkBookmark(currentUrl);
|
||
|
||
if (existingBookmark) {
|
||
await syncManager.syncBookmark({
|
||
title: document.title,
|
||
description: '',
|
||
url: currentUrl,
|
||
notes: notes
|
||
}, folder);
|
||
|
||
Utils.showMessage('Bookmark updated', 'success');
|
||
} else {
|
||
await syncManager.syncBookmark({
|
||
title: document.title,
|
||
description: '',
|
||
url: currentUrl,
|
||
notes: notes
|
||
}, folder);
|
||
|
||
Utils.showMessage('Bookmark added', 'success');
|
||
}
|
||
|
||
// Reset form
|
||
document.getElementById('bookmark-url').value = '';
|
||
document.getElementById('bookmark-notes').value = '';
|
||
document.getElementById('bookmark-folder').value = '';
|
||
|
||
} catch (error) {
|
||
console.error('Sync error:', error);
|
||
Utils.showMessage('Failed to sync bookmark: ' + error.message, 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
const btnSettings = document.getElementById('btn-settings');
|
||
if (btnSettings) {
|
||
btnSettings.addEventListener('click', async () => {
|
||
await browser.runtime.openOptionsPage();
|
||
});
|
||
}
|
||
});
|
||
|
||
function updateStatus() {
|
||
const statusIcon = document.getElementById('status-icon');
|
||
const statusTitle = document.getElementById('status-title');
|
||
const statusMessage = document.getElementById('status-message');
|
||
const syncInfo = document.getElementById('sync-info');
|
||
const bundleNameEl = document.getElementById('bundle-name');
|
||
const syncModeEl = document.getElementById('sync-mode-display');
|
||
const lastSyncEl = document.getElementById('last-sync');
|
||
|
||
if (!syncInfo) {
|
||
return;
|
||
}
|
||
|
||
checkConnection().then(canConnect => {
|
||
if (!canConnect) {
|
||
statusIcon.textContent = '✕';
|
||
statusTitle.textContent = 'Connection Error';
|
||
statusMessage.textContent = 'Unable to connect to Linkding';
|
||
syncInfo.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
syncInfo.style.display = 'block';
|
||
|
||
const config = new Store().get();
|
||
|
||
config.then(conf => {
|
||
new SyncManager().getBundleInfo().then(bundleInfo => {
|
||
if (bundleInfo) {
|
||
bundleNameEl.textContent = bundleInfo.bundleTag || '-';
|
||
syncModeEl.textContent = conf.syncMode || 'bi-directional';
|
||
|
||
const lastSync = bundleInfo.lastSync;
|
||
lastSyncEl.textContent = Utils.formatTimestamp(lastSync);
|
||
|
||
statusIcon.textContent = '✓';
|
||
statusTitle.textContent = 'Connected';
|
||
statusMessage.textContent = 'Syncing with Linkding';
|
||
}
|
||
});
|
||
});
|
||
|
||
// Default status for now
|
||
statusIcon.textContent = '⏳';
|
||
statusTitle.textContent = 'Loading...';
|
||
statusMessage.textContent = 'Connecting to Linkding...';
|
||
}).catch(error => {
|
||
console.error('Status update error:', error);
|
||
statusIcon.textContent = '✕';
|
||
statusTitle.textContent = 'Connection Error';
|
||
statusMessage.textContent = error.message || 'Unable to connect';
|
||
syncInfo.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
function checkConnection() {
|
||
const config = new Store().get();
|
||
|
||
return config.then(conf => {
|
||
if (!conf.serverUrl) {
|
||
return false;
|
||
}
|
||
|
||
return fetch(`${conf.serverUrl}api/bookmarks/?limit=1`, {
|
||
headers: {
|
||
'Authorization': `Token ${conf.apiKey}`
|
||
}
|
||
})
|
||
.then(response => {
|
||
if (response.ok) {
|
||
return true;
|
||
}
|
||
return false;
|
||
})
|
||
.catch(error => {
|
||
console.error('Connection check error:', error);
|
||
return false;
|
||
});
|
||
});
|
||
} |