Initial commit of MyWorkspace - contains multiple projects and global workspace configuration
This commit is contained in:
132
Linkding Browser Extension/LinkdingSync/utils/bookmark.js
Normal file
132
Linkding Browser Extension/LinkdingSync/utils/bookmark.js
Normal file
@@ -0,0 +1,132 @@
|
||||
// Bookmark Manipulation Utilities
|
||||
// Handles bookmark operations for synchronization
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const BookmarkUtils = {
|
||||
|
||||
// Parse structured notes from Linkding bookmark
|
||||
parseNotes(noteString) {
|
||||
if (!noteString || typeof noteString !== 'string') {
|
||||
return {
|
||||
path: '',
|
||||
userNotes: '',
|
||||
autoTags: []
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(noteString);
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, treat as plain notes
|
||||
return {
|
||||
path: '',
|
||||
userNotes: noteString,
|
||||
autoTags: []
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Build structured notes object
|
||||
buildNotes(path, userNotes, autoTags) {
|
||||
return {
|
||||
path: path || '',
|
||||
userNotes: userNotes || '',
|
||||
autoTags: autoTags || []
|
||||
};
|
||||
},
|
||||
|
||||
// Format notes for storage (always as JSON string)
|
||||
formatNotes(notesObject) {
|
||||
return JSON.stringify(notesObject);
|
||||
},
|
||||
|
||||
// Extract folder path from bookmark
|
||||
extractPath(bookmark) {
|
||||
const notes = this.parseNotes(bookmark.notes || '');
|
||||
return notes.path || '';
|
||||
},
|
||||
|
||||
// Get display notes (userNotes only)
|
||||
getDisplayNotes(bookmark) {
|
||||
const notes = this.parseNotes(bookmark.notes || '');
|
||||
return notes.userNotes || '';
|
||||
},
|
||||
|
||||
// Get auto-generated tags
|
||||
getAutoTags(bookmark) {
|
||||
const notes = this.parseNotes(bookmark.notes || '');
|
||||
return notes.autoTags || [];
|
||||
},
|
||||
|
||||
// Create bookmark for Linkding API
|
||||
createBookmarkData(url, title, description, notes, folder) {
|
||||
return {
|
||||
url: url,
|
||||
title: title || '',
|
||||
description: description || '',
|
||||
notes: this.formatNotes(this.buildNotes(folder || '', notes || '')),
|
||||
unread: false,
|
||||
shared: false,
|
||||
tag_names: []
|
||||
};
|
||||
},
|
||||
|
||||
// Update bookmark for Linkding API
|
||||
updateBookmarkData(bookmarkId, title, description, notes, folder) {
|
||||
return {
|
||||
id: bookmarkId,
|
||||
title: title || '',
|
||||
description: description || '',
|
||||
notes: this.formatNotes(this.buildNotes(folder || '', notes || '')),
|
||||
unread: false,
|
||||
shared: false
|
||||
};
|
||||
},
|
||||
|
||||
// Check if bookmark exists by URL
|
||||
async checkExistingBookmark(url) {
|
||||
const response = await fetch(`${window.linkdingSyncBaseURL}api/bookmarks/check/?url=${encodeURIComponent(url)}`, {
|
||||
headers: {
|
||||
'Authorization': `Token ${window.linkdingSyncApiKey}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
// Merge bookmark data from browser and Linkding
|
||||
mergeBookmarks(browserBookmark, linkdingBookmark) {
|
||||
if (!linkdingBookmark) {
|
||||
return browserBookmark;
|
||||
}
|
||||
|
||||
// Keep browser's path, merge notes
|
||||
const browserNotes = this.parseNotes(browserBookmark.notes || '');
|
||||
const linkdingNotes = this.parseNotes(linkdingBookmark.notes || '');
|
||||
|
||||
// Use browser notes as primary
|
||||
const mergedNotes = {
|
||||
path: browserNotes.path || linkdingNotes.path || '',
|
||||
userNotes: browserNotes.userNotes || linkdingNotes.userNotes || '',
|
||||
autoTags: browserNotes.autoTags || linkdingNotes.autoTags || []
|
||||
};
|
||||
|
||||
return {
|
||||
...linkdingBookmark,
|
||||
notes: this.formatNotes(mergedNotes),
|
||||
title: browserBookmark.title || linkdingBookmark.title,
|
||||
description: browserBookmark.description || linkdingBookmark.description
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Export for use in other scripts
|
||||
window.BookmarkUtils = BookmarkUtils;
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,229 @@
|
||||
// Conflict Resolution Utilities
|
||||
// Handles bookmark conflict resolution strategies
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const ConflictResolver = {
|
||||
|
||||
// Sync mode configurations
|
||||
MODES: {
|
||||
'bi-directional': {
|
||||
name: 'bi-directional',
|
||||
description: 'Bi-directional Sync',
|
||||
keepBoth: true,
|
||||
prioritizeLinkding: false,
|
||||
prioritizeBrowser: false
|
||||
},
|
||||
'write-only': {
|
||||
name: 'write-only',
|
||||
description: 'Write-Only (Browser Authoritative)',
|
||||
keepBoth: false,
|
||||
prioritizeLinkding: false,
|
||||
prioritizeBrowser: true
|
||||
},
|
||||
'read-only': {
|
||||
name: 'read-only',
|
||||
description: 'Read-Only (Linkding Authoritative)',
|
||||
keepBoth: false,
|
||||
prioritizeLinkding: true,
|
||||
prioritizeBrowser: false
|
||||
}
|
||||
},
|
||||
|
||||
// Get mode config
|
||||
getModeConfig(mode) {
|
||||
return this.MODES[mode] || this.MODES['bi-directional'];
|
||||
},
|
||||
|
||||
// Resolve conflict when same URL exists in different locations
|
||||
resolve(browserBookmark, linkdingBookmark, browserNotes, linkdingNotes, mode, action) {
|
||||
const config = this.getModeConfig(mode);
|
||||
const timestamp = Date.now();
|
||||
|
||||
switch (action) {
|
||||
case 'create':
|
||||
// New bookmark in browser
|
||||
return {
|
||||
action: 'create',
|
||||
result: {
|
||||
...linkdingBookmark,
|
||||
notes: this.formatNotes(browserNotes),
|
||||
title: browserBookmark.title,
|
||||
description: browserBookmark.description
|
||||
},
|
||||
note: `Created new bookmark: ${browserBookmark.url}`,
|
||||
timestamp: timestamp
|
||||
};
|
||||
|
||||
case 'update':
|
||||
// Update existing bookmark
|
||||
if (config.keepBoth) {
|
||||
// Bi-directional: keep Linkding data, merge browser notes
|
||||
return {
|
||||
action: 'update',
|
||||
result: {
|
||||
...linkdingBookmark,
|
||||
notes: this.mergeNotes(linkdingNotes, browserNotes),
|
||||
title: browserBookmark.title || linkdingBookmark.title,
|
||||
description: browserBookmark.description || linkdingBookmark.description
|
||||
},
|
||||
note: `Updated existing bookmark. Kept Linkding data.`,
|
||||
timestamp: timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// Write-only or read-only: use browser data
|
||||
return {
|
||||
action: 'update',
|
||||
result: {
|
||||
...linkdingBookmark,
|
||||
notes: this.formatNotes(browserNotes),
|
||||
title: browserBookmark.title,
|
||||
description: browserBookmark.description
|
||||
},
|
||||
note: `Updated from browser`,
|
||||
timestamp: timestamp
|
||||
};
|
||||
|
||||
case 'delete':
|
||||
// Bookmark deleted
|
||||
if (config.prioritizeBrowser) {
|
||||
return {
|
||||
action: 'delete',
|
||||
result: linkdingBookmark,
|
||||
note: `Deleted from Linkding (browser is authoritative)`,
|
||||
timestamp: timestamp
|
||||
};
|
||||
}
|
||||
|
||||
if (config.prioritizeLinkding) {
|
||||
return {
|
||||
action: 'ignore',
|
||||
result: linkdingBookmark,
|
||||
note: `Kept Linkding version (read-only mode)`,
|
||||
timestamp: timestamp
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'delete',
|
||||
result: linkdingBookmark,
|
||||
note: `Deleted from Linkding`,
|
||||
timestamp: timestamp
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
action: 'none',
|
||||
result: linkdingBookmark,
|
||||
note: 'No action needed',
|
||||
timestamp: timestamp
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Format notes object for storage
|
||||
formatNotes(notes) {
|
||||
if (!notes) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
path: notes.path || '',
|
||||
userNotes: notes.userNotes || '',
|
||||
autoTags: notes.autoTags || []
|
||||
});
|
||||
},
|
||||
|
||||
// Merge two notes objects
|
||||
mergeNotes(notes1, notes2) {
|
||||
const p1 = this.parseNotes(notes1);
|
||||
const p2 = this.parseNotes(notes2);
|
||||
|
||||
return {
|
||||
path: p1.path || p2.path,
|
||||
userNotes: p1.userNotes || p2.userNotes,
|
||||
autoTags: [...(p1.autoTags || []), ...(p2.autoTags || [])]
|
||||
.filter((v, i, a) => a.indexOf(v) === i)
|
||||
};
|
||||
},
|
||||
|
||||
// Parse notes string
|
||||
parseNotes(noteString) {
|
||||
if (!noteString) {
|
||||
return {
|
||||
path: '',
|
||||
userNotes: '',
|
||||
autoTags: []
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(noteString);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return {
|
||||
path: parsed.path || '',
|
||||
userNotes: parsed.userNotes || '',
|
||||
autoTags: parsed.autoTags || []
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid JSON
|
||||
}
|
||||
|
||||
return {
|
||||
path: '',
|
||||
userNotes: noteString,
|
||||
autoTags: []
|
||||
};
|
||||
},
|
||||
|
||||
// Get conflict report
|
||||
getConflictReport(browserBookmark, linkdingBookmark) {
|
||||
const browserNotes = this.parseNotes(browserBookmark.notes || '');
|
||||
const linkdingNotes = this.parseNotes(linkdingBookmark.notes || '');
|
||||
|
||||
return {
|
||||
url: browserBookmark.url,
|
||||
browserPath: browserNotes.path,
|
||||
linkdingPath: linkdingNotes.path,
|
||||
browserNotes: browserNotes.userNotes,
|
||||
linkdingNotes: linkdingNotes.userNotes,
|
||||
differs: browserNotes.path !== linkdingNotes.path ||
|
||||
browserNotes.userNotes !== linkdingNotes.userNotes
|
||||
};
|
||||
},
|
||||
|
||||
// Check if sync is needed
|
||||
needsSync(browserBookmark, linkdingBookmark) {
|
||||
if (!linkdingBookmark) {
|
||||
return true; // New bookmark
|
||||
}
|
||||
|
||||
const browserNotes = this.parseNotes(browserBookmark.notes || '');
|
||||
const linkdingNotes = this.parseNotes(linkdingBookmark.notes || '');
|
||||
|
||||
// Check if paths differ
|
||||
if (browserNotes.path !== linkdingNotes.path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if notes differ
|
||||
if (browserNotes.userNotes !== linkdingNotes.userNotes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if titles differ
|
||||
if (browserBookmark.title !== linkdingBookmark.title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Export for use in other scripts
|
||||
window.ConflictResolver = ConflictResolver;
|
||||
|
||||
})();
|
||||
123
Linkding Browser Extension/LinkdingSync/utils/notes-parser.js
Normal file
123
Linkding Browser Extension/LinkdingSync/utils/notes-parser.js
Normal file
@@ -0,0 +1,123 @@
|
||||
// Notes Parser Utilities
|
||||
// Handles parsing and formatting of structured notes
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const NotesParser = {
|
||||
|
||||
// Parse raw notes string into structured object
|
||||
parse(noteString) {
|
||||
if (!noteString) {
|
||||
return {
|
||||
path: '',
|
||||
userNotes: '',
|
||||
autoTags: []
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(noteString);
|
||||
|
||||
// Validate structure
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return {
|
||||
path: parsed.path || '',
|
||||
userNotes: parsed.userNotes || '',
|
||||
autoTags: parsed.autoTags || []
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid JSON, treat as plain notes
|
||||
}
|
||||
|
||||
return {
|
||||
path: '',
|
||||
userNotes: noteString,
|
||||
autoTags: []
|
||||
};
|
||||
},
|
||||
|
||||
// Format structured notes for storage
|
||||
format(notesObject) {
|
||||
if (!notesObject) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const notes = {
|
||||
path: notesObject.path || '',
|
||||
userNotes: notesObject.userNotes || '',
|
||||
autoTags: notesObject.autoTags || []
|
||||
};
|
||||
|
||||
return JSON.stringify(notes);
|
||||
},
|
||||
|
||||
// Extract just the user notes (for display)
|
||||
extractUserNotes(noteString) {
|
||||
const parsed = this.parse(noteString);
|
||||
return parsed.userNotes;
|
||||
},
|
||||
|
||||
// Extract just the path (for sync operations)
|
||||
extractPath(noteString) {
|
||||
const parsed = this.parse(noteString);
|
||||
return parsed.path;
|
||||
},
|
||||
|
||||
// Extract auto-generated tags
|
||||
extractAutoTags(noteString) {
|
||||
const parsed = this.parse(noteString);
|
||||
return parsed.autoTags || [];
|
||||
},
|
||||
|
||||
// Generate tags from path
|
||||
generateTagsFromPath(path) {
|
||||
if (!path) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Split by slash and filter empty
|
||||
const folders = path.split('/').filter(f => f);
|
||||
|
||||
return folders.map(folder => ({
|
||||
name: folder.trim()
|
||||
}));
|
||||
},
|
||||
|
||||
// Merge two notes objects
|
||||
merge(notes1, notes2) {
|
||||
const parsed1 = this.parse(notes1);
|
||||
const parsed2 = this.parse(notes2);
|
||||
|
||||
return {
|
||||
path: parsed1.path || parsed2.path,
|
||||
userNotes: parsed1.userNotes || parsed2.userNotes,
|
||||
autoTags: [...(parsed1.autoTags || []), ...(parsed2.autoTags || [])]
|
||||
.filter((v, i, a) => a.indexOf(v) === i) // Remove duplicates
|
||||
};
|
||||
},
|
||||
|
||||
// Validate notes structure
|
||||
validate(notesObject) {
|
||||
if (!notesObject || typeof notesObject !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!notesObject.path || !!notesObject.userNotes;
|
||||
},
|
||||
|
||||
// Create minimal notes object
|
||||
createEmpty() {
|
||||
return {
|
||||
path: '',
|
||||
userNotes: '',
|
||||
autoTags: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Export for use in other scripts
|
||||
window.NotesParser = NotesParser;
|
||||
|
||||
})();
|
||||
148
Linkding Browser Extension/LinkdingSync/utils/sync.js
Normal file
148
Linkding Browser Extension/LinkdingSync/utils/sync.js
Normal file
@@ -0,0 +1,148 @@
|
||||
// Synchronization Utilities
|
||||
// Handles sync logic and conflict resolution
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const SyncUtils = {
|
||||
|
||||
// Sync mode descriptions
|
||||
MODES: {
|
||||
'bi-directional': {
|
||||
name: 'Bi-Directional',
|
||||
description: 'Keep both versions; additions/updates replicate both ways',
|
||||
deleteFromLinkding: true,
|
||||
keepBothOnUpdate: true
|
||||
},
|
||||
'write-only': {
|
||||
name: 'Write-Only',
|
||||
description: 'Browser is authoritative; updates push to Linkding only',
|
||||
deleteFromLinkding: true,
|
||||
keepBothOnUpdate: false
|
||||
},
|
||||
'read-only': {
|
||||
name: 'Read-Only',
|
||||
description: 'Linkding is authoritative; download from Linkding only',
|
||||
deleteFromLinkding: false,
|
||||
keepBothOnUpdate: false
|
||||
}
|
||||
},
|
||||
|
||||
// Get sync mode config
|
||||
getModeConfig(mode) {
|
||||
return this.MODES[mode] || this.MODES['bi-directional'];
|
||||
},
|
||||
|
||||
// Determine sync action based on mode
|
||||
determineSyncAction(mode, action) {
|
||||
const config = this.getModeConfig(mode);
|
||||
|
||||
switch (action) {
|
||||
case 'create':
|
||||
// New bookmark in browser
|
||||
if (config.deleteFromLinkding) {
|
||||
return { action: 'create', deleteFromLinkding: false };
|
||||
}
|
||||
return { action: 'create' };
|
||||
|
||||
case 'update':
|
||||
// Existing bookmark with changes
|
||||
if (config.keepBothOnUpdate) {
|
||||
return { action: 'update', keepBoth: true };
|
||||
}
|
||||
return { action: 'update' };
|
||||
|
||||
case 'delete':
|
||||
// Bookmark deleted from browser
|
||||
if (config.deleteFromLinkding) {
|
||||
return { action: 'delete' };
|
||||
}
|
||||
return { action: 'ignore' };
|
||||
|
||||
default:
|
||||
return { action: 'none' };
|
||||
}
|
||||
},
|
||||
|
||||
// Resolve conflict when same URL exists in different folders
|
||||
resolveConflict(browserBookmark, linkdingBookmark, browserNotes, linkdingNotes, mode) {
|
||||
const config = this.getModeConfig(mode);
|
||||
|
||||
if (config.keepBothOnUpdate) {
|
||||
// Bi-directional: keep both, use Linkding notes as primary
|
||||
const mergedNotes = {
|
||||
path: browserNotes.path || linkdingNotes.path,
|
||||
userNotes: config.keepBothOnUpdate ? linkdingNotes.userNotes : browserNotes.userNotes,
|
||||
autoTags: []
|
||||
};
|
||||
|
||||
return {
|
||||
action: 'update',
|
||||
notes: mergedNotes,
|
||||
note: 'Updated with browser path; Linkding notes preserved'
|
||||
};
|
||||
}
|
||||
|
||||
// Write-only or read-only: use browser data
|
||||
return {
|
||||
action: 'update',
|
||||
notes: browserNotes,
|
||||
note: 'Updated from browser'
|
||||
};
|
||||
},
|
||||
|
||||
// Check if bookmark needs sync
|
||||
needsSync(browserBookmark, linkdingBookmark, browserNotes, linkdingNotes) {
|
||||
// Check if URLs match
|
||||
if (browserBookmark.url !== linkdingBookmark.url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if path changed
|
||||
if (browserNotes.path !== linkdingNotes.path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if notes changed
|
||||
if (browserNotes.userNotes !== linkdingNotes.userNotes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if tags changed
|
||||
const browserTags = browserNotes.autoTags.map(t => t.name);
|
||||
const linkdingTags = linkdingNotes.autoTags.map(t => t.name);
|
||||
|
||||
if (JSON.stringify(browserTags) !== JSON.stringify(linkdingTags)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// Get last sync timestamp
|
||||
getLastSyncTime() {
|
||||
return new Date().toISOString();
|
||||
},
|
||||
|
||||
// Format sync timestamp for display
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// Export for use in other scripts
|
||||
window.SyncUtils = SyncUtils;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user