feat(prefs): export/import Saved Points (merge + dedupe by id) in Preferences dialog

This commit is contained in:
hermes-explorigin 2026-08-10 15:28:35 +00:00
parent fe072c8d85
commit 5c29320e6b

View File

@ -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<string | null>(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 @@
</select>
</div>
<div class="pref-hint">Distances use m/ft under a km/mile, otherwise km/mi. Saved to this browser (localStorage).</div>
<div class="pref-rule"></div>
<div class="pref-title small">Saved Points ({savedPointsList.length})</div>
<div class="pref-row">
<span class="pref-label">Back up / restore your saved points</span>
</div>
<div class="pref-btns">
<button class="pref-small-btn" onclick={() => exportSavedPointsToFile()} disabled={!savedPointsList.length}>Export</button>
<label class="pref-file">
Import…
<input type="file" accept="application/json,.json" hidden onchange={(e) => importSavedPointsFromFile((e.target as HTMLInputElement).files?.[0] ?? null)} />
</label>
</div>
{#if importMsg}<div class="pref-import-msg">{importMsg}</div>{/if}
<button class="pref-close" onclick={() => (showPrefs = false)}>Done</button>
</div>
</div>
@ -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; }