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,682 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LinkdingSync Settings</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
color: white;
padding: 20px;
text-align: center;
}
header h1 {
font-size: 24px;
margin-bottom: 8px;
}
header p {
font-size: 14px;
opacity: 0.9;
}
.settings-section {
padding: 20px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #e0e0e0;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: #555;
margin-bottom: 6px;
}
.form-group input[type="text"],
.form-group input[type="url"],
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #2196f3;
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
}
.form-group textarea {
min-height: 80px;
resize: vertical;
}
.form-group select {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
background: white;
cursor: pointer;
}
.hint {
font-size: 12px;
color: #777;
margin-top: 6px;
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
}
.hint code {
background: #e0e0e0;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
font-size: 11px;
}
.sync-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.sync-option {
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
font-weight: 500;
}
.sync-option:hover {
border-color: #2196f3;
background: #f5f5f5;
}
.sync-option.selected {
border-color: #2196f3;
background: #e3f2fd;
color: #1565c0;
}
.sync-option .description {
display: block;
font-size: 11px;
font-weight: normal;
margin-top: 4px;
opacity: 0.8;
}
.sync-info {
padding: 16px;
background: #f5f5f5;
border-radius: 8px;
margin-bottom: 16px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 13px;
border-bottom: 1px solid #e0e0e0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: #666;
}
.info-value {
font-family: monospace;
color: #333;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 12px;
}
.btn-primary {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
}
.btn-secondary {
background: white;
color: #666;
border: 1px solid #ddd;
}
.btn-secondary:hover {
background: #f5f5f5;
}
.btn-danger {
background: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
.btn-danger:hover {
background: #ffcdd2;
}
.btn:active {
transform: translateY(0);
}
.btn-group {
display: flex;
gap: 8px;
margin-top: 12px;
}
.save-status {
padding: 12px;
text-align: center;
font-size: 13px;
border-radius: 6px;
margin-bottom: 16px;
display: none;
}
.save-status.success {
background: #e8f5e9;
color: #2e7d32;
display: block;
}
.save-status.error {
background: #ffebee;
color: #c62828;
display: block;
}
.reset-section {
padding: 16px;
border-top: 2px solid #e0e0e0;
text-align: center;
font-size: 13px;
color: #777;
}
.reset-section button {
margin-top: 8px;
padding: 8px 16px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
}
.reset-section button:hover {
background: #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>⚙️ LinkdingSync Settings</h1>
<p>Configure your bookmark synchronization</p>
</header>
<div class="settings-section">
<div class="section-title">Connection Settings</div>
<div class="form-group">
<label for="server-url">Linkding Server URL</label>
<input type="url" id="server-url" placeholder="https://links.blabber1565.com" value="https://links.blabber1565.com">
<div class="hint">
Enter your Linkding instance URL. This should point to the root of your Linkding installation.<br>
Example: <code>https://links.blabber1565.com</code>
</div>
</div>
<div class="form-group">
<label for="api-key">API Token</label>
<input type="text" id="api-key" placeholder="Enter your API token">
<div class="hint">
Get your API token from Linkding Settings → Advanced → API Token<br>
This token is stored locally in your browser's storage.<br>
<code>Settings</code><code>Advanced</code><code>API Token</code>
</div>
</div>
</div>
<div class="settings-section">
<div class="section-title">Bundle Configuration</div>
<div class="form-group">
<label for="bundle-tag">Bundle Tag</label>
<input type="text" id="bundle-tag" placeholder="bundle_personal_firefox_1">
<div class="hint">
This tag defines which bookmarks belong to which bundle in Linkding.<br>
All bookmarks with this tag will appear in the corresponding Linkding bundle.<br>
Example: <code>bundle_personal_firefox_1</code>
</div>
</div>
</div>
<div class="settings-section">
<div class="section-title">Synchronization Options</div>
<div class="sync-options">
<div class="sync-option selected" data-mode="bi-directional">
<strong>Bi-Directional</strong>
<span class="description">Keep both versions<br>Additions/updates replicate both ways</span>
</div>
<div class="sync-option" data-mode="write-only">
<strong>Write-Only</strong>
<span class="description">Browser is authoritative<br>Updates push to Linkding only</span>
</div>
<div class="sync-option" data-mode="read-only">
<strong>Read-Only</strong>
<span class="description">Linkding is authoritative<br>Download from Linkding only</span>
</div>
</div>
<div class="form-group">
<label for="sync-timestamp">Last Sync</label>
<div class="info-value" id="sync-timestamp">-</div>
</div>
<div class="form-group">
<label for="auto-tags">Auto-Generate Tags</label>
<select id="auto-tags">
<option value="false" selected>Disabled</option>
<option value="true">Enabled</option>
</select>
<div class="hint">
When enabled, extracts folder names from bookmark path as tag suggestions.
</div>
</div>
</div>
<div class="settings-section">
<div class="section-title">Actions</div>
<button class="btn btn-primary" id="save-btn">💾 Save Settings</button>
<div id="save-status" class="save-status"></div>
<div class="btn-group">
<button class="btn btn-secondary" id="test-connection-btn">🔌 Test Connection</button>
<button class="btn btn-secondary" id="refresh-bundles-btn">🔄 Refresh Bundles</button>
</div>
</div>
<div class="settings-section">
<div class="section-title">Connection Info</div>
<div class="sync-info">
<div class="info-row">
<span class="info-label">Server URL:</span>
<span class="info-value" id="info-server-url">-</span>
</div>
<div class="info-row">
<span class="info-label">Bundle Tag:</span>
<span class="info-value" id="info-bundle-tag">-</span>
</div>
<div class="info-row">
<span class="info-label">Sync Mode:</span>
<span class="info-value" id="info-sync-mode">-</span>
</div>
<div class="info-row">
<span class="info-label">API Token Set:</span>
<span class="info-value" id="info-api-token">-</span>
</div>
</div>
</div>
<div class="reset-section">
<p>Need to reset your configuration?</p>
<button id="reset-config-btn">Reset All Settings</button>
</div>
</div>
<script>
(function() {
'use strict';
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);
},
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;
const existing = document.getElementById('notification-area');
if (existing) existing.remove();
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);
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 4000);
}
};
const storage = {
get: browser.storage.local.get.bind(browser.storage.local),
set: browser.storage.local.set.bind(browser.storage.local),
clear: browser.storage.local.clear.bind(browser.storage.local)
};
const api = {
getBundles: async function() {
const response = await fetch(`${window.linkdingSyncBaseURL}api/bookmarks/?limit=100&any=any`, {
headers: { 'Authorization': `Token ${window.linkdingSyncApiKey}` }
});
if (!response.ok) {
return { results: [], count: 0 };
}
return await response.json();
},
getBundle: async function(bundleTag) {
const response = await fetch(`${window.linkdingSyncBaseURL}api/bookmarks/?limit=1000&any=${encodeURIComponent(bundleTag)}`, {
headers: { 'Authorization': `Token ${window.linkdingSyncApiKey}` }
});
if (!response.ok) {
return { results: [], count: 0 };
}
return await response.json();
}
};
const store = {
get: async function(keys) {
const data = await storage.get(keys);
return {
serverUrl: data.serverUrl || 'https://links.blabber1565.com',
apiKey: data.apiKey || '',
bundleTag: data.bundleTag || '',
syncMode: data.syncMode || 'bi-directional',
autoGenerateTags: data.autoGenerateTags || false,
lastSyncTime: data.lastSyncTime || null
};
},
set: async function(config) {
await storage.set(config);
},
clear: async function() {
await storage.clear();
}
};
// Initialize UI
document.addEventListener('DOMContentLoaded', async () => {
try {
const config = await store.get();
// Populate form
document.getElementById('server-url').value = config.serverUrl;
document.getElementById('api-key').value = config.apiKey;
document.getElementById('bundle-tag').value = config.bundleTag || 'bundle_personal_firefox_1';
// Sync mode selection
const syncOptions = document.querySelectorAll('.sync-option');
syncOptions.forEach(option => {
option.addEventListener('click', () => {
syncOptions.forEach(o => o.classList.remove('selected'));
option.classList.add('selected');
document.getElementById('info-sync-mode').textContent = option.querySelector('strong').textContent;
config.syncMode = option.dataset.mode;
store.set({ syncMode: option.dataset.mode });
});
});
// Highlight selected sync mode
const selectedMode = document.querySelector(`.sync-option[data-mode="${config.syncMode}"]`);
if (selectedMode) {
syncOptions.forEach(o => o.classList.remove('selected'));
selectedMode.classList.add('selected');
}
// Populate info
document.getElementById('info-server-url').textContent = config.serverUrl;
document.getElementById('info-bundle-tag').textContent = config.bundleTag || '-';
document.getElementById('info-sync-mode').textContent = config.syncMode || 'bi-directional';
document.getElementById('info-api-token').textContent = config.apiKey ? '✓' : '-';
document.getElementById('sync-timestamp').textContent = Utils.formatTimestamp(config.lastSyncTime);
} catch (error) {
console.error('Load config error:', error);
}
// Save button
document.getElementById('save-btn').addEventListener('click', async () => {
const serverUrl = document.getElementById('server-url').value.trim();
const apiKey = document.getElementById('api-key').value.trim();
const bundleTag = document.getElementById('bundle-tag').value.trim();
const selectedMode = document.querySelector('.sync-option.selected');
if (!apiKey) {
Utils.showMessage('Please enter an API token', 'error');
return;
}
if (!serverUrl) {
Utils.showMessage('Please enter a server URL', 'error');
return;
}
const config = {
serverUrl: serverUrl,
apiKey: apiKey,
bundleTag: bundleTag || 'bundle_default',
syncMode: selectedMode ? selectedMode.dataset.mode : 'bi-directional',
autoGenerateTags: document.getElementById('auto-tags').value === 'true' ? true : false
};
try {
await store.set(config);
Utils.showMessage('Settings saved!', 'success');
// Update info display
document.getElementById('info-server-url').textContent = config.serverUrl;
document.getElementById('info-bundle-tag').textContent = config.bundleTag || '-';
document.getElementById('info-sync-mode').textContent = config.syncMode || 'bi-directional';
document.getElementById('info-api-token').textContent = config.apiKey ? '✓' : '-';
document.getElementById('sync-timestamp').textContent = Utils.formatTimestamp(config.lastSyncTime);
} catch (error) {
console.error('Save error:', error);
Utils.showMessage('Failed to save settings: ' + error.message, 'error');
}
});
// Test connection
document.getElementById('test-connection-btn').addEventListener('click', async () => {
const config = await store.get();
if (!config.serverUrl || !config.apiKey) {
Utils.showMessage('Please configure connection first', 'warning');
return;
}
try {
const response = await fetch(`${config.serverUrl}api/bookmarks/?limit=1`, {
headers: { 'Authorization': `Token ${config.apiKey}` }
});
if (response.ok) {
Utils.showMessage('Connection successful!', 'success');
} else {
Utils.showMessage('Connection failed: HTTP ' + response.status, 'error');
}
} catch (error) {
Utils.showMessage('Connection error: ' + error.message, 'error');
}
});
// Refresh bundles
document.getElementById('refresh-bundles-btn').addEventListener('click', async () => {
const config = await store.get();
if (!config.bundleTag) {
Utils.showMessage('Please configure bundle tag first', 'warning');
return;
}
try {
const result = await api.getBundle(config.bundleTag);
Utils.showMessage(`Found ${result.count || 0} bookmarks`, 'success');
} catch (error) {
Utils.showMessage('Failed to refresh: ' + error.message, 'error');
}
});
// Reset config
document.getElementById('reset-config-btn').addEventListener('click', async () => {
if (confirm('This will delete all settings. Continue?')) {
await store.clear();
Utils.showMessage('Settings cleared', 'info');
// Clear form
document.getElementById('server-url').value = 'https://links.blabber1565.com';
document.getElementById('api-key').value = '';
document.getElementById('bundle-tag').value = 'bundle_personal_firefox_1';
// Reset info
document.getElementById('info-server-url').textContent = '-';
document.getElementById('info-bundle-tag').textContent = '-';
document.getElementById('info-sync-mode').textContent = 'bi-directional';
document.getElementById('info-api-token').textContent = '-';
}
});
// Auto-update bundle tag display when changed
document.getElementById('bundle-tag').addEventListener('change', (e) => {
document.getElementById('info-bundle-tag').textContent = e.target.value || '-';
});
// Listen for storage changes
browser.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.bundleTag) {
document.getElementById('bundle-tag').value = changes.bundleTag.newValue;
document.getElementById('info-bundle-tag').textContent = changes.bundleTag.newValue || '-';
}
});
});
})();
</script>
</body>
</html>