Skip to content

Commit b2f564a

Browse files
authored
Merge pull request #20 from pheuberger/claude/export-import-settings-PamJQ
Implement bookmark import/export with Netscape HTML format
2 parents cbbb8b9 + 2842617 commit b2f564a

4 files changed

Lines changed: 934 additions & 19 deletions

File tree

package-lock.json

Lines changed: 18 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/ui/SettingsView.jsx

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
import { useState, useEffect } from 'react'
1+
import { useState, useEffect, useRef } from 'react'
22
import PairingFlow from '../pairing/PairingFlow'
33
import { cn } from '@/utils/cn'
44
import { subscribeToWebrtcProvider } from '../../hooks/useYjs'
55
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'
77
import { SettingSection, SettingRow, SettingCard, SettingsContainer } from './SettingsLayout'
88
import { RelayConfigurationView } from './RelayConfigurationView'
99
import { DiagnosticsView } from './DiagnosticsView'
1010
import { performFullReset, checkResetableData } from '../../services/reset'
11+
import { downloadExport, importFromFile } from '../../services/bookmark-io'
1112

1213
export function SettingsView({ onBack }) {
1314
const [showPairing, setShowPairing] = useState(false)
1415
const [showRelayConfig, setShowRelayConfig] = useState(false)
1516
const [showDiagnostics, setShowDiagnostics] = useState(false)
1617
const [connected, setConnected] = useState(false)
1718
const [peerCount, setPeerCount] = useState(0)
19+
const [importStatus, setImportStatus] = useState(null)
20+
const fileInputRef = useRef(null)
1821

1922
// Reset state
2023
const [showResetConfirm, setShowResetConfirm] = useState(false)
@@ -104,6 +107,53 @@ export function SettingsView({ onBack }) {
104107
return `${Math.floor(seconds / 3600)}h ago`
105108
}
106109

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+
107157
const handleResetClick = async () => {
108158
const data = await checkResetableData()
109159
setResetData(data)
@@ -391,28 +441,48 @@ export function SettingsView({ onBack }) {
391441
<SettingCard>
392442
<SettingRow
393443
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)"
395445
>
396446
<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"
399449
>
400-
Coming soon
450+
<Download className="w-4 h-4" />
451+
Export
401452
</button>
402453
</SettingRow>
403454
<SettingRow
404455
label="Import bookmarks"
405-
description="Import bookmarks from a JSON or HTML file"
456+
description="Import bookmarks from a browser export file (HTML)"
406457
isLast
407458
>
459+
<input
460+
ref={fileInputRef}
461+
type="file"
462+
accept=".html,.htm"
463+
onChange={handleFileChange}
464+
className="hidden"
465+
/>
408466
<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"
411469
>
412-
Coming soon
470+
<Upload className="w-4 h-4" />
471+
Import
413472
</button>
414473
</SettingRow>
415474
</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+
)}
416486
</SettingSection>
417487

418488
<SettingSection title="About">

0 commit comments

Comments
 (0)