|
1 | | -import { useState, useEffect } from 'react' |
| 1 | +import { useState, useEffect, useRef } from 'react' |
2 | 2 | import PairingFlow from '../pairing/PairingFlow' |
3 | 3 | import { cn } from '@/utils/cn' |
4 | 4 | import { subscribeToWebrtcProvider } from '../../hooks/useYjs' |
5 | 5 | import { useNostrSync } from '../../hooks/useNostrSync' |
6 | | -import { ChevronLeft, Cloud, CloudOff, RefreshCw, Settings2, ChevronRight, Activity, Smartphone, AlertTriangle, Trash2 } from 'lucide-react' |
| 6 | +import { ChevronLeft, Cloud, CloudOff, RefreshCw, Settings2, ChevronRight, Activity, Smartphone, AlertTriangle, Trash2, Download, Upload } from 'lucide-react' |
7 | 7 | import { SettingSection, SettingRow, SettingCard, SettingsContainer } from './SettingsLayout' |
8 | 8 | import { RelayConfigurationView } from './RelayConfigurationView' |
9 | 9 | import { DiagnosticsView } from './DiagnosticsView' |
10 | 10 | import { performFullReset, checkResetableData } from '../../services/reset' |
| 11 | +import { downloadExport, importFromFile } from '../../services/bookmark-io' |
11 | 12 |
|
12 | 13 | export function SettingsView({ onBack }) { |
13 | 14 | const [showPairing, setShowPairing] = useState(false) |
14 | 15 | const [showRelayConfig, setShowRelayConfig] = useState(false) |
15 | 16 | const [showDiagnostics, setShowDiagnostics] = useState(false) |
16 | 17 | const [connected, setConnected] = useState(false) |
17 | 18 | const [peerCount, setPeerCount] = useState(0) |
| 19 | + const [importStatus, setImportStatus] = useState(null) |
| 20 | + const fileInputRef = useRef(null) |
18 | 21 |
|
19 | 22 | // Reset state |
20 | 23 | const [showResetConfirm, setShowResetConfirm] = useState(false) |
@@ -104,6 +107,53 @@ export function SettingsView({ onBack }) { |
104 | 107 | return `${Math.floor(seconds / 3600)}h ago` |
105 | 108 | } |
106 | 109 |
|
| 110 | +const handleExport = () => { |
| 111 | + try { |
| 112 | + downloadExport() |
| 113 | + } catch (err) { |
| 114 | + console.error('[Settings] Export failed:', err) |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + const handleImportClick = () => { |
| 119 | + fileInputRef.current?.click() |
| 120 | + } |
| 121 | + |
| 122 | + const handleFileChange = async (e) => { |
| 123 | + const file = e.target.files?.[0] |
| 124 | + if (!file) return |
| 125 | + |
| 126 | + setImportStatus({ type: 'loading', message: 'Importing...' }) |
| 127 | + |
| 128 | + try { |
| 129 | + const result = await importFromFile(file) |
| 130 | + if (result.imported > 0) { |
| 131 | + setImportStatus({ |
| 132 | + type: 'success', |
| 133 | + message: `Imported ${result.imported} bookmark${result.imported === 1 ? '' : 's'}${result.skipped > 0 ? `, ${result.skipped} skipped (duplicates)` : ''}`, |
| 134 | + }) |
| 135 | + } else if (result.skipped > 0) { |
| 136 | + setImportStatus({ |
| 137 | + type: 'info', |
| 138 | + message: `All ${result.skipped} bookmark${result.skipped === 1 ? '' : 's'} already exist`, |
| 139 | + }) |
| 140 | + } else { |
| 141 | + setImportStatus({ |
| 142 | + type: 'error', |
| 143 | + message: 'No bookmarks found in file', |
| 144 | + }) |
| 145 | + } |
| 146 | + } catch (err) { |
| 147 | + setImportStatus({ type: 'error', message: `Import failed: ${err.message}` }) |
| 148 | + } |
| 149 | + |
| 150 | + // Clear file input so same file can be selected again |
| 151 | + e.target.value = '' |
| 152 | + |
| 153 | + // Clear status after 5 seconds |
| 154 | + setTimeout(() => setImportStatus(null), 5000) |
| 155 | + } |
| 156 | + |
107 | 157 | const handleResetClick = async () => { |
108 | 158 | const data = await checkResetableData() |
109 | 159 | setResetData(data) |
@@ -391,28 +441,48 @@ export function SettingsView({ onBack }) { |
391 | 441 | <SettingCard> |
392 | 442 | <SettingRow |
393 | 443 | label="Export bookmarks" |
394 | | - description="Download all your bookmarks as a JSON file" |
| 444 | + description="Download all bookmarks as an HTML file (browser-compatible format)" |
395 | 445 | > |
396 | 446 | <button |
397 | | - disabled |
398 | | - className="text-sm font-medium text-muted-foreground/50 cursor-not-allowed" |
| 447 | + onClick={handleExport} |
| 448 | + className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors" |
399 | 449 | > |
400 | | - Coming soon |
| 450 | + <Download className="w-4 h-4" /> |
| 451 | + Export |
401 | 452 | </button> |
402 | 453 | </SettingRow> |
403 | 454 | <SettingRow |
404 | 455 | label="Import bookmarks" |
405 | | - description="Import bookmarks from a JSON or HTML file" |
| 456 | + description="Import bookmarks from a browser export file (HTML)" |
406 | 457 | isLast |
407 | 458 | > |
| 459 | + <input |
| 460 | + ref={fileInputRef} |
| 461 | + type="file" |
| 462 | + accept=".html,.htm" |
| 463 | + onChange={handleFileChange} |
| 464 | + className="hidden" |
| 465 | + /> |
408 | 466 | <button |
409 | | - disabled |
410 | | - className="text-sm font-medium text-muted-foreground/50 cursor-not-allowed" |
| 467 | + onClick={handleImportClick} |
| 468 | + className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors" |
411 | 469 | > |
412 | | - Coming soon |
| 470 | + <Upload className="w-4 h-4" /> |
| 471 | + Import |
413 | 472 | </button> |
414 | 473 | </SettingRow> |
415 | 474 | </SettingCard> |
| 475 | + {importStatus && ( |
| 476 | + <p className={cn( |
| 477 | + "text-xs mt-2 px-1", |
| 478 | + importStatus.type === 'success' && "text-green-600 dark:text-green-400", |
| 479 | + importStatus.type === 'error' && "text-red-600 dark:text-red-400", |
| 480 | + importStatus.type === 'info' && "text-muted-foreground", |
| 481 | + importStatus.type === 'loading' && "text-muted-foreground" |
| 482 | + )}> |
| 483 | + {importStatus.message} |
| 484 | + </p> |
| 485 | + )} |
416 | 486 | </SettingSection> |
417 | 487 |
|
418 | 488 | <SettingSection title="About"> |
|
0 commit comments