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.
+
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 + '' +
- '
' + 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 + '' + 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 + '' +
+ '
' + 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 + '' + 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(