From 5c29320e6b3a834d639da225b892903266c55e25 Mon Sep 17 00:00:00 2001 From: hermes-explorigin Date: Mon, 10 Aug 2026 15:28:35 +0000 Subject: [PATCH] feat(prefs): export/import Saved Points (merge + dedupe by id) in Preferences dialog --- src/lib/components/MapView.svelte | 64 ++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/lib/components/MapView.svelte b/src/lib/components/MapView.svelte index e6f13b0..005d3dc 100644 --- a/src/lib/components/MapView.svelte +++ b/src/lib/components/MapView.svelte @@ -7,7 +7,7 @@ import DirectionsPanel from '$lib/components/DirectionsPanel.svelte'; import { prefs } from '$lib/prefs'; import type { RouteGraph, AvoidRule, RouteResult } from '$lib/routing'; - import { savedPoints, addSavedPoint, removeSavedPoint, type SavedPoint } from '$lib/savedPoints'; + import { savedPoints, addSavedPoint, exportSavedPoints, importSavedPoints, type SavedPoint } from '$lib/savedPoints'; type FeatureCollection = { type: string; @@ -271,6 +271,43 @@ savedPointsVisible = !savedPointsVisible; void drawSavedPoints(); } + let importMsg = $state(null); + + // Download the current saved points as JSON. + function exportSavedPointsToFile() { + const json = exportSavedPoints(); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `navigator-saved-points-${new Date().toISOString().slice(0, 10)}.json`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } + + // Read a selected file and merge its saved points into the store. + function importSavedPointsFromFile(file: File | null) { + importMsg = null; + if (!file) return; + if (file.type && file.type !== 'application/json' && !file.name.endsWith('.json')) { + importMsg = 'Please choose a .json file.'; + return; + } + const reader = new FileReader(); + reader.onload = () => { + try { + const { added, skipped } = importSavedPoints(String(reader.result)); + importMsg = `Imported ${added} point${added === 1 ? '' : 's'}${skipped ? `, skipped ${skipped} existing` : ''}.`; + } catch (e) { + importMsg = e instanceof Error ? e.message : 'Import failed.'; + } + }; + reader.onerror = () => { importMsg = 'Could not read the file.'; }; + reader.readAsText(file); + } + function setSavedPointAsOrigin(p: SavedPoint) { setOrigin(p.lat, p.lon, p.label || 'Saved point'); } @@ -569,6 +606,21 @@
Distances use m/ft under a km/mile, otherwise km/mi. Saved to this browser (localStorage).
+ +
+
Saved Points ({savedPointsList.length})
+
+ Back up / restore your saved points +
+
+ + +
+ {#if importMsg}
{importMsg}
{/if} + @@ -685,6 +737,16 @@ background: #0f172a; color: #fff; font-weight: 600; cursor: pointer; font-size: 0.85rem; } .pref-close:hover { background: #1e293b; } + .pref-rule { border-top: 1px solid #e2e8f0; margin: 14px 0 8px; } + .pref-title.small { font-size: 0.82rem; margin-bottom: 6px; } + .pref-btns { display: flex; gap: 8px; margin-top: 4px; } + .pref-small-btn, .pref-file { + display: inline-flex; align-items: center; padding: 6px 12px; border-radius: 6px; + border: 1px solid #cbd5e1; background: #fff; cursor: pointer; font-size: 0.78rem; color: #0f172a; + } + .pref-small-btn:hover, .pref-file:hover { background: #f1f5f9; } + .pref-small-btn:disabled { opacity: .5; cursor: default; } + .pref-import-msg { font-size: 0.72rem; color: #475569; margin-top: 8px; } /* saved points pin */ :global(.saved-pin-wrap) { background: transparent; border: none; } :global(.saved-pin) { width: 0; height: 0; border: 10px solid transparent; border-bottom: 0; border-left-color: #8b5cf6; }