diff --git a/package.json b/package.json index 98fa59fc..889e165c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.77.0", + "version": "1.77.1", "private": true, "type": "module", "scripts": { diff --git a/src/lib/components/hikes/route-builder/ImageDropzone.svelte b/src/lib/components/hikes/route-builder/ImageDropzone.svelte index ba470015..48a25feb 100644 --- a/src/lib/components/hikes/route-builder/ImageDropzone.svelte +++ b/src/lib/components/hikes/route-builder/ImageDropzone.svelte @@ -6,11 +6,15 @@ scheduleSave, type Waypoint } from './builderStore.svelte'; + // `untrack` keeps the in-loop `builder.waypoints.find(...)` from + // registering as a dep on a non-reactive call site, avoiding effect + // loops when we patch the matched waypoint's `thumbnail`. + import { untrack } from 'svelte'; import { generateImageHashClient } from '$lib/imageHashClient'; import { readThumbnail } from './imageThumbnail'; import { setFullImage } from './fullImageCache.svelte'; - type Status = 'pending' | 'placed' | 'unplaced' | 'error'; + type Status = 'pending' | 'placed' | 'unplaced' | 'matched' | 'error'; type Entry = { id: string; @@ -22,8 +26,17 @@ let entries = $state([]); let isDragging = $state(false); + // Counts hash-only image waypoints (typically restored from a GPX + // import) that don't yet have a thumbnail — surfaces a contextual + // hint in the dropzone header so the user knows that dropping the + // source JPEGs here will attach previews to those rows in the table. + const orphanImageCount = $derived( + builder.waypoints.filter((w) => w.imageHash && !w.thumbnail).length + ); + type Prepared = - | { ok: true; wp: Waypoint; hasGps: boolean; id: string; file: File } + | { ok: true; kind: 'new'; wp: Waypoint; hasGps: boolean; id: string; file: File } + | { ok: true; kind: 'matched'; id: string; file: File } | { ok: false }; async function handleFiles(files: File[]) { @@ -49,6 +62,30 @@ thumbnail = await readThumbnail(file); } catch { /* preview is optional */ } const imageHash = await generateImageHashClient(file); + + // Match path: if a previously-imported (or earlier-dropped) + // waypoint already carries this content hash, attach the + // thumbnail to it instead of creating a duplicate marker. + // Covers the GPX-roundtrip flow where the user loads an + // existing GPX (image hashes restored as bare waypoints) + // and then drops the source images to give them previews. + const existing = untrack(() => + builder.waypoints.find((w) => w.imageHash === imageHash) + ); + if (existing) { + if (thumbnail && !existing.thumbnail) existing.thumbnail = thumbnail; + // Trust the imported visibility if the existing waypoint + // already has one set — re-dropping shouldn't silently + // flip a private photo to public. + if (!existing.imageVisibility) existing.imageVisibility = 'public'; + scheduleSave(); + entries[entryIdx].status = 'matched'; + entries[entryIdx].message = existing.unplaced + ? 'noch nicht auf der Karte platziert' + : undefined; + return { ok: true, kind: 'matched', id: existing.id, file }; + } + const timestamp = exif?.DateTimeOriginal instanceof Date ? exif.DateTimeOriginal.getTime() : null; @@ -82,7 +119,7 @@ }; entries[entryIdx].status = hasGps ? 'placed' : 'unplaced'; - return { ok: true, wp, hasGps, id, file }; + return { ok: true, kind: 'new', wp, hasGps, id, file }; } catch (err) { entries[entryIdx].status = 'error'; entries[entryIdx].message = (err as Error).message; @@ -100,10 +137,11 @@ // returning 0 even with workaround attempts. for (const p of prepared) { if (!p.ok) continue; - insertWaypointChronologically(p.wp); + if (p.kind === 'new') insertWaypointChronologically(p.wp); // Cache the original file so the waypoint table can show a - // full-resolution preview this session. Persistence to - // localStorage keeps only the small thumbnail. + // full-resolution preview this session (for both new + matched + // waypoints). Persistence to localStorage keeps only the small + // thumbnail. setFullImage(p.id, p.file); } } @@ -151,6 +189,14 @@ erscheinen in der Wegpunkt-Liste und können dort auf der Karte platziert werden. Die Bilder verlassen dein Gerät nicht.

+ {#if orphanImageCount > 0} +

+ {orphanImageCount} + {orphanImageCount === 1 ? 'Bild-Wegpunkt' : 'Bild-Wegpunkte'} aus der + geladenen GPX warten auf eine Vorschau — die Original-Bilder hier ablegen, + um sie über den Inhalts-Hash automatisch zuzuordnen. +

+ {/if}