feat: add progressive nextcloud loading
This commit is contained in:
@@ -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 {
|
||||
<button class="secondary" id="use-demo" type="button">Demo anzeigen</button>
|
||||
</div>
|
||||
<div class="status" id="share-status">Demo-Daten aktiv.</div>
|
||||
<div class="progress" aria-label="Ladefortschritt">
|
||||
<div class="progress-bar" aria-hidden="true">
|
||||
<div class="progress-fill" id="progress-fill"></div>
|
||||
</div>
|
||||
<div class="progress-meta">
|
||||
<span id="progress-text">0 / 0</span>
|
||||
<span id="progress-detail">bereit</span>
|
||||
</div>
|
||||
</div>
|
||||
<small>Hinweis: Öffentliche Nextcloud-Shares werden per WebDAV gelesen.</small>
|
||||
</section>
|
||||
|
||||
@@ -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,19 +616,27 @@ function htmlPage(): string {
|
||||
}
|
||||
});
|
||||
|
||||
function renderPhotos(photos) {
|
||||
state.photos = photos;
|
||||
markers.clearLayers();
|
||||
route.setLatLngs([]);
|
||||
photoList.replaceChildren();
|
||||
photoCount.textContent = photos.length + (photos.length === 1 ? " Bild" : " Bilder");
|
||||
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]);
|
||||
|
||||
const bounds = [];
|
||||
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");
|
||||
|
||||
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(
|
||||
'<div class="thumb">' +
|
||||
@@ -590,19 +658,17 @@ function htmlPage(): string {
|
||||
'<div><strong>' + photo.name + '</strong><span>' + formatDate(photo.capturedAt) + '</span></div>';
|
||||
item.addEventListener("click", () => openOverlay(photo));
|
||||
photoList.appendChild(item);
|
||||
|
||||
updateRouteView();
|
||||
}
|
||||
|
||||
const routePoints = 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);
|
||||
}
|
||||
|
||||
if (bounds.length > 0) {
|
||||
map.fitBounds(bounds, { padding: [40, 40] });
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user