Initial commit of MyWorkspace - contains multiple projects and global workspace configuration

This commit is contained in:
DavidSaylor
2026-05-06 22:59:37 -05:00
commit 019e35b488
2520 changed files with 13634 additions and 0 deletions

View 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;
})();

View File

@@ -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;
})();

View 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;
})();

View 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;
})();