feat(prefs): export/import Saved Points (merge + dedupe by id) in Preferences dialog
This commit is contained in:
parent
fe072c8d85
commit
5c29320e6b
@ -7,7 +7,7 @@
|
|||||||
import DirectionsPanel from '$lib/components/DirectionsPanel.svelte';
|
import DirectionsPanel from '$lib/components/DirectionsPanel.svelte';
|
||||||
import { prefs } from '$lib/prefs';
|
import { prefs } from '$lib/prefs';
|
||||||
import type { RouteGraph, AvoidRule, RouteResult } from '$lib/routing';
|
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 FeatureCollection = {
|
||||||
type: string;
|
type: string;
|
||||||
@ -271,6 +271,43 @@
|
|||||||
savedPointsVisible = !savedPointsVisible;
|
savedPointsVisible = !savedPointsVisible;
|
||||||
void drawSavedPoints();
|
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) {
|
function setSavedPointAsOrigin(p: SavedPoint) {
|
||||||
setOrigin(p.lat, p.lon, p.label || 'Saved point');
|
setOrigin(p.lat, p.lon, p.label || 'Saved point');
|
||||||
}
|
}
|
||||||
@ -569,6 +606,21 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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-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>
|
<button class="pref-close" onclick={() => (showPrefs = false)}>Done</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -685,6 +737,16 @@
|
|||||||
background: #0f172a; color: #fff; font-weight: 600; cursor: pointer; font-size: 0.85rem;
|
background: #0f172a; color: #fff; font-weight: 600; cursor: pointer; font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
.pref-close:hover { background: #1e293b; }
|
.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 */
|
/* saved points pin */
|
||||||
:global(.saved-pin-wrap) { background: transparent; border: none; }
|
: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; }
|
:global(.saved-pin) { width: 0; height: 0; border: 10px solid transparent; border-bottom: 0; border-left-color: #8b5cf6; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user