Files
myworkspace/Linkding Browser Extension/LinkdingSync/background.html

682 lines
20 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>