Initial commit of MyWorkspace - contains multiple projects and global workspace configuration
This commit is contained in:
873
Linkding Browser Extension/LinkdingSync/background.js
Normal file
873
Linkding Browser Extension/LinkdingSync/background.js
Normal file
@@ -0,0 +1,873 @@
|
||||
// LinkdingSync Background Service Worker
|
||||
// Handles synchronization logic, bookmark management, and API calls
|
||||
|
||||
'use strict';
|
||||
|
||||
// Direct browser.storage.local API (no prefix in service worker)
|
||||
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 (no prefix in service worker)
|
||||
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
|
||||
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
|
||||
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 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 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 '';
|
||||
}
|
||||
},
|
||||
|
||||
extractTagsFromPath(path) {
|
||||
if (!path) return [];
|
||||
|
||||
const folders = path.split('/').filter(f => f);
|
||||
return folders.map(folder => ({
|
||||
name: folder.trim()
|
||||
}));
|
||||
},
|
||||
|
||||
async showNotification(message, type = 'info', duration = 4000) {
|
||||
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 duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Global Sync Manager
|
||||
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 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 getBundle(bundleTag) {
|
||||
// First check if bundle exists
|
||||
let bundleData = await this.getBundleDetails(this.bundleId);
|
||||
|
||||
if (bundleData) {
|
||||
// Bundle exists, get bookmarks with this tag
|
||||
// Use pagination to handle more than 1000 bookmarks
|
||||
const allBookmarks = [];
|
||||
let hasMore = true;
|
||||
let offset = 0;
|
||||
const pageSize = 100;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await fetch(
|
||||
`${this.config.serverUrl}api/bookmarks/?limit=${pageSize}&all=${encodeURIComponent(bundleData.all_tags || '')}&offset=${offset}`,
|
||||
{ headers: await this.api.getAuthHeaders() }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
break;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
allBookmarks.push(...(data.results || []));
|
||||
|
||||
if (data.results?.length < pageSize) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
offset += pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bundleId: bundleData.id,
|
||||
bundleTag: bundleData.name || bundleTag,
|
||||
count: allBookmarks.length,
|
||||
results: allBookmarks
|
||||
};
|
||||
}
|
||||
|
||||
// Bundle doesn't exist yet, create it
|
||||
bundleData = await this.createBundle(bundleTag);
|
||||
|
||||
// Create empty bundle
|
||||
return { results: [], count: 0, bundleId: bundleData.id };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async syncAllBookmarks() {
|
||||
const config = await this.store.get();
|
||||
|
||||
if (!config.apiKey || !config.serverUrl) {
|
||||
console.log('Not configured for sync');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.init(config);
|
||||
const bundleInfo = await this.getBundleInfo();
|
||||
|
||||
if (!bundleInfo) {
|
||||
console.log('No bundle available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch bookmarks from Linkding with bundle filter
|
||||
const linkdingBookmarks = await this.getBundle(bundleInfo.bundleTag);
|
||||
|
||||
// Get browser bookmarks
|
||||
const browserBookmarks = await bookmarks.getTree();
|
||||
|
||||
// Sync each bookmark
|
||||
for (const bookmark of browserBookmarks) {
|
||||
await this.syncBookmark(bookmark, '', this.config.syncMode);
|
||||
}
|
||||
|
||||
this.lastSyncTime = Date.now();
|
||||
await this.store.set({ lastSyncTime: this.lastSyncTime });
|
||||
|
||||
Utils.showNotification(`Synced ${linkdingBookmarks.count || 0} bookmarks`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Sync all error:', error);
|
||||
Utils.showNotification('Sync failed: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Export for console access
|
||||
window.LinkdingSync = {
|
||||
ApiClient: ApiClient,
|
||||
Store: Store,
|
||||
Utils: Utils,
|
||||
NotesManager: NotesManager,
|
||||
Manager: SyncManager
|
||||
};
|
||||
|
||||
// Initialize when storage changes
|
||||
browser.storage.onChanged.addListener((changes, area) => {
|
||||
if (area === 'local') {
|
||||
console.log('Configuration changed:', changes);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for bookmark changes
|
||||
browser.bookmarks.onCreated.addListener(async (bookmark) => {
|
||||
console.log('Bookmark created:', bookmark.id);
|
||||
|
||||
const manager = new SyncManager();
|
||||
const config = await manager.store.get();
|
||||
if (config) {
|
||||
manager.syncBookmark(bookmark, '', config.syncMode);
|
||||
}
|
||||
});
|
||||
|
||||
browser.bookmarks.onChanged.addListener(async (change, bookmarkId) => {
|
||||
console.log('Bookmark changed:', bookmarkId);
|
||||
|
||||
const manager = new SyncManager();
|
||||
const bookmarkData = await browser.bookmarks.get(bookmarkId);
|
||||
if (bookmarkData && bookmarkData[0]) {
|
||||
const config = await manager.store.get();
|
||||
if (config) {
|
||||
manager.syncBookmark(bookmarkData[0], '', config.syncMode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
browser.bookmarks.onRemoved.addListener(async (removed, bookmarkId) => {
|
||||
console.log('Bookmark removed:', bookmarkId);
|
||||
|
||||
const manager = new SyncManager();
|
||||
const config = await manager.store.get();
|
||||
if (config) {
|
||||
manager.deleteBookmark(removed.url);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
console.log('linkdingsync initialized');
|
||||
Reference in New Issue
Block a user