diff --git a/src/server/request-handler.ts b/src/server/request-handler.ts index 3fb70cc..dc37a08 100644 --- a/src/server/request-handler.ts +++ b/src/server/request-handler.ts @@ -250,6 +250,35 @@ function htmlPage(): string { color: var(--muted); } + .progress { + display: grid; + gap: 8px; + margin-top: 12px; + } + + .progress-bar { + height: 10px; + border-radius: 999px; + overflow: hidden; + background: rgba(15, 23, 42, 0.1); + } + + .progress-fill { + width: 0%; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--accent), #67b2ff); + transition: width 180ms ease; + } + + .progress-meta { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 0.82rem; + color: var(--muted); + } + .list { display: grid; gap: 10px; @@ -420,6 +449,15 @@ function htmlPage(): string {
Demo-Daten aktiv.
+
+ +
+ 0 / 0 + bereit +
+
Hinweis: Öffentliche Nextcloud-Shares werden per WebDAV gelesen. @@ -453,7 +491,9 @@ function htmlPage(): string { const DEMO_PHOTOS = ${JSON.stringify(demoPhotos)}; const state = { photos: DEMO_PHOTOS, - objectUrls: [] + objectUrls: [], + processed: 0, + total: 0 }; const map = L.map("map", { zoomControl: true }).setView([52.5208, 13.4095], 13); @@ -475,6 +515,9 @@ function htmlPage(): string { const closeOverlay = mustGet("close-overlay"); const photoList = mustGet("photo-list"); const photoCount = mustGet("photo-count"); + const progressFill = mustGet("progress-fill"); + const progressText = mustGet("progress-text"); + const progressDetail = mustGet("progress-detail"); const shareUrl = mustGet("share-url"); const shareStatus = mustGet("share-status"); const loadShare = mustGet("load-share"); @@ -529,6 +572,14 @@ function htmlPage(): string { shareStatus.style.color = tone === "error" ? "#9d174d" : "var(--muted)"; } + function setProgress(processed, total, detail) { + state.processed = processed; + state.total = total; + progressFill.style.width = total > 0 ? Math.round((processed / total) * 100) + "%" : "0%"; + progressText.textContent = processed + " / " + total; + progressDetail.textContent = detail; + } + function clearObjectUrls() { for (const url of state.objectUrls) { URL.revokeObjectURL(url); @@ -536,6 +587,15 @@ function htmlPage(): string { state.objectUrls = []; } + function clearGallery() { + state.photos = []; + markers.clearLayers(); + route.setLatLngs([]); + photoList.replaceChildren(); + photoCount.textContent = "0 Bilder"; + setProgress(0, 0, "bereit"); + } + function openOverlay(photo) { overlay.classList.add("open"); overlay.setAttribute("aria-hidden", "false"); @@ -556,53 +616,59 @@ function htmlPage(): string { } }); - function renderPhotos(photos) { - state.photos = photos; - markers.clearLayers(); - route.setLatLngs([]); - photoList.replaceChildren(); - photoCount.textContent = photos.length + (photos.length === 1 ? " Bild" : " Bilder"); - - const bounds = []; - - for (const photo of photos) { - if (photo.latitude !== null && photo.longitude !== null) { - const marker = L.marker([photo.latitude, photo.longitude]).addTo(markers); - bounds.push([photo.latitude, photo.longitude]); - - marker.bindPopup( - '
' + - '' + photo.name + '' + - '' + photo.name + '' + - '' + formatDate(photo.capturedAt) + '' + - '' + - '
' - ); - - marker.on("mouseover", () => marker.openPopup()); - marker.on("click", () => openOverlay(photo)); - } - - const item = document.createElement("article"); - item.className = "photo"; - item.innerHTML = - '' + photo.name + '' + - '
' + photo.name + '' + formatDate(photo.capturedAt) + '
'; - item.addEventListener("click", () => openOverlay(photo)); - photoList.appendChild(item); - } - - const routePoints = photos + function updateRouteView() { + const routePoints = state.photos .filter((photo) => photo.latitude !== null && photo.longitude !== null && photo.capturedAt) .sort((a, b) => String(a.capturedAt).localeCompare(String(b.capturedAt))) .map((photo) => [photo.latitude, photo.longitude]); - if (routePoints.length > 1) { - route.setLatLngs(routePoints); + route.setLatLngs(routePoints); + + if (routePoints.length === 1) { + map.setView(routePoints[0], 13); + } else if (routePoints.length > 1) { + map.fitBounds(route.getBounds(), { padding: [40, 40] }); + } + } + + function appendPhoto(photo) { + state.photos.push(photo); + photoCount.textContent = state.photos.length + (state.photos.length === 1 ? " Bild" : " Bilder"); + + if (photo.latitude !== null && photo.longitude !== null) { + const marker = L.marker([photo.latitude, photo.longitude]).addTo(markers); + + marker.bindPopup( + '
' + + '' + photo.name + '' + + '' + photo.name + '' + + '' + formatDate(photo.capturedAt) + '' + + '' + + '
' + ); + + marker.on("mouseover", () => marker.openPopup()); + marker.on("click", () => openOverlay(photo)); } - if (bounds.length > 0) { - map.fitBounds(bounds, { padding: [40, 40] }); + const item = document.createElement("article"); + item.className = "photo"; + item.innerHTML = + '' + photo.name + '' + + '
' + photo.name + '' + formatDate(photo.capturedAt) + '
'; + item.addEventListener("click", () => openOverlay(photo)); + photoList.appendChild(item); + + updateRouteView(); + } + + function renderPhotos(photos) { + clearGallery(); + let processed = 0; + for (const photo of photos) { + appendPhoto(photo); + processed += 1; + setProgress(processed, photos.length, "Demo geladen"); } } @@ -707,6 +773,8 @@ function htmlPage(): string { async function importFromNextcloud() { try { + clearObjectUrls(); + clearGallery(); updateStatus("Share wird geladen..."); const { davUrl } = parseShareInput(shareUrl.value); const listing = await loadShareListing(davUrl); @@ -715,35 +783,53 @@ function htmlPage(): string { throw new Error("Im Share wurden keine Bilder gefunden."); } - const loaded = []; - const skipped = []; + setProgress(0, listing.length, "Dateien werden geprüft"); + + let loaded = 0; + let skipped = 0; + let processed = 0; for (const entry of listing) { try { const photo = await readRemotePhoto(entry); if (photo.latitude !== null && photo.longitude !== null) { - loaded.push(photo); + appendPhoto(photo); + loaded += 1; } else { - skipped.push(entry.name + " (kein GPS)"); + skipped += 1; } } catch (error) { - skipped.push(entry.name); + skipped += 1; console.error(error); } + + processed += 1; + setProgress( + processed, + listing.length, + "geladen " + loaded + ", übersprungen " + skipped + ); + updateStatus( + "Import läuft: " + + loaded + + " Bilder angezeigt, " + + skipped + + " übersprungen." + ); } - if (!loaded.length) { + if (!loaded) { throw new Error("Keine Bilder mit GPS-Daten gefunden."); } - renderPhotos(loaded); updateStatus( "Nextcloud-Import fertig: " + - loaded.length + + loaded + " Bilder geladen" + - (skipped.length ? ", " + skipped.length + " übersprungen" : "") + + (skipped ? ", " + skipped + " übersprungen" : "") + "." ); + setProgress(listing.length, listing.length, "fertig"); } catch (error) { console.error(error); updateStatus(