This commit is contained in:
+140
-5
@@ -42,6 +42,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
routeListView: 'editor-route-list-view',
|
||||
routeName: 'editor-route-name',
|
||||
routeSave: 'editor-route-save',
|
||||
routeUpload: 'editor-route-upload',
|
||||
status: 'editor-status'
|
||||
});
|
||||
|
||||
@@ -240,7 +241,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
async function handleResponse(response) {
|
||||
if (response.ok) return parseJson(response);
|
||||
const message = await response.text();
|
||||
throw new Error(message || `HTTP ${response.status}`);
|
||||
const error = new Error(message || `HTTP ${response.status}`);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function readJson(path) {
|
||||
@@ -468,9 +471,19 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
byId(ids.routeName).value = routeName(route) === 'Unbenannte Strecke' ? '' : routeName(route);
|
||||
byId(ids.routeDescription).value = text(route?.description);
|
||||
byId(ids.routeGpx).value = '';
|
||||
|
||||
const uploadButton = byId(ids.routeUpload);
|
||||
if (state.currentSource === 'local') {
|
||||
byId(ids.routeActivated).textContent = 'Lokaler Entwurf: GPX, Texte und Medien bleiben ausschließlich im Browser. Eine Server-Freigabe ist noch nicht implementiert.';
|
||||
const uploaded = Boolean(route?.serverRouteId);
|
||||
setHidden(uploadButton, false);
|
||||
setDisabled(uploadButton, uploaded);
|
||||
if (uploadButton) uploadButton.textContent = uploaded ? 'Bereits übertragen' : 'An Server übertragen';
|
||||
byId(ids.routeActivated).textContent = uploaded
|
||||
? `Lokaler Entwurf. Bereits als unveröffentlichte Server-Strecke ${route.serverRouteId} zur Prüfung übertragen.`
|
||||
: 'Lokaler Entwurf: GPX, Texte und Medien bleiben im Browser, bis „An Server übertragen“ gewählt wird.';
|
||||
} else {
|
||||
setHidden(uploadButton, true);
|
||||
setDisabled(uploadButton, true);
|
||||
byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at
|
||||
? `Veröffentlicht am ${route.activatedAt || route.activated_at}. Der Editor verändert die Freigabe nicht.`
|
||||
: 'Server-Strecke ohne Freigabedatum. Der Editor verändert activated_at nicht.';
|
||||
@@ -697,7 +710,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
const file = byId(ids.routeGpx)?.files?.[0];
|
||||
data.append('name', values.name);
|
||||
data.append('description', values.description);
|
||||
if (file) data.append('file', file);
|
||||
if (file) data.append('gpx', file);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -727,7 +740,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
state.pois = saved.pois;
|
||||
await loadRoutes();
|
||||
await loadRoute({ id: saved.id, _source: 'local' });
|
||||
setStatus('Strecke erfolgreich lokal gespeichert. Es wurden keine Daten an den Server übertragen.');
|
||||
setStatus('Speichern erfolgreich.');
|
||||
}
|
||||
|
||||
async function saveServerRoute(values) {
|
||||
@@ -742,7 +755,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
state.currentRoute = { ...saved, _source: 'server' };
|
||||
await loadRoutes();
|
||||
await loadRoute({ id, _source: 'server' });
|
||||
setStatus('Strecke erfolgreich gespeichert. activated_at wurde nicht verändert.');
|
||||
setStatus('Speichern erfolgreich.');
|
||||
}
|
||||
|
||||
async function saveRoute() {
|
||||
@@ -752,6 +765,127 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
else await saveLocalRoute(values);
|
||||
}
|
||||
|
||||
function safeGpxFilename(name) {
|
||||
const base = text(name || 'wegwichtel-route')
|
||||
.normalize('NFKD')
|
||||
.replaceAll(/[^a-zA-Z0-9_-]+/g, '-')
|
||||
.replaceAll(/^-+|-+$/g, '')
|
||||
.slice(0, 80);
|
||||
return `${base || 'wegwichtel-route'}.gpx`;
|
||||
}
|
||||
|
||||
function localRouteUploadForm(routePackage) {
|
||||
const data = new FormData();
|
||||
data.append('name', routePackage.route?.name || 'Unbenannte Strecke');
|
||||
data.append('description', routePackage.route?.description || '');
|
||||
const gpx = new Blob([routePackage.gpx], { type: 'application/gpx+xml' });
|
||||
data.append('gpx', gpx, safeGpxFilename(routePackage.route?.name));
|
||||
return data;
|
||||
}
|
||||
|
||||
async function storedMediaBlob(media) {
|
||||
if (!media?.dataUrl) return null;
|
||||
const response = await fetch(media.dataUrl);
|
||||
if (!response.ok || typeof response.blob !== 'function') {
|
||||
throw new Error(`Die Mediendatei ${media.name || ''} konnte nicht für den Upload vorbereitet werden.`.trim());
|
||||
}
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
async function uploadStoredMedia(path, fieldName, media, fallbackName) {
|
||||
const blob = await storedMediaBlob(media);
|
||||
if (!blob) return;
|
||||
const data = new FormData();
|
||||
data.append(fieldName, blob, media.name || fallbackName);
|
||||
await sendForm(path, 'POST', data);
|
||||
}
|
||||
|
||||
async function uploadLocalPoiMedia(serverRouteId, serverPoiId, poi) {
|
||||
const basePath = `/routes/${encodeURIComponent(serverRouteId)}/pois/${encodeURIComponent(serverPoiId)}`;
|
||||
if (poi.audio?.dataUrl) {
|
||||
await uploadStoredMedia(`${basePath}/audio`, 'audio', poi.audio, 'audio.bin');
|
||||
}
|
||||
for (const [index, image] of (poi.images || []).entries()) {
|
||||
if (!image?.dataUrl) continue;
|
||||
await uploadStoredMedia(`${basePath}/pictures`, 'picture', image, `bild-${index + 1}.bin`);
|
||||
}
|
||||
}
|
||||
|
||||
async function createServerRouteFromLocal(routePackage) {
|
||||
let createdRouteId = null;
|
||||
try {
|
||||
const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage));
|
||||
const serverRoute = payload.route || payload;
|
||||
createdRouteId = routeId(serverRoute);
|
||||
if (!createdRouteId) throw new Error('Der Server hat keine Strecken-ID für den Upload zurückgegeben.');
|
||||
|
||||
const orderedPois = [...(routePackage.pois || [])]
|
||||
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
|
||||
for (const [index, poi] of orderedPois.entries()) {
|
||||
const poiPayload = await sendJson(`/routes/${encodeURIComponent(createdRouteId)}/pois`, 'POST', {
|
||||
description: poi.description || '',
|
||||
lat: poi.lat,
|
||||
lon: poi.lon,
|
||||
sequence: index,
|
||||
title: poi.title || `Station ${index + 1}`,
|
||||
triggerRadiusM: poi.triggerRadiusM || 25
|
||||
});
|
||||
const serverPoi = poiPayload.poi || poiPayload;
|
||||
const serverPoiId = poiId(serverPoi);
|
||||
if (!serverPoiId) throw new Error(`Für ${poi.title || `Station ${index + 1}`} wurde keine Server-POI-ID zurückgegeben.`);
|
||||
await uploadLocalPoiMedia(createdRouteId, serverPoiId, poi);
|
||||
}
|
||||
return createdRouteId;
|
||||
} catch (error) {
|
||||
if (createdRouteId) {
|
||||
try {
|
||||
await sendDelete(`/routes/${encodeURIComponent(createdRouteId)}`);
|
||||
} catch {
|
||||
// Der ursprüngliche Uploadfehler ist wichtiger als ein fehlgeschlagener Rollback.
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadLocalRoute() {
|
||||
if (state.currentSource !== 'local') throw new Error('Nur lokale Strecken können hochgeladen werden.');
|
||||
const values = routeFromForm();
|
||||
if (!values.name) throw new Error('Der Name der Strecke ist erforderlich.');
|
||||
if (state.currentRoute?.serverRouteId) {
|
||||
setStatus('Diese Strecke wurde bereits an den Server übertragen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadButton = byId(ids.routeUpload);
|
||||
setDisabled(uploadButton, true);
|
||||
try {
|
||||
await saveLocalRoute(values);
|
||||
setStatus('Übertragung läuft …');
|
||||
const localId = routeId(state.currentRoute);
|
||||
const routePackage = ns.LocalRoutes.get(localId);
|
||||
if (!routePackage) throw new Error('Der lokale Streckenentwurf wurde nicht gefunden.');
|
||||
|
||||
const serverRouteId = await createServerRouteFromLocal(routePackage);
|
||||
const uploadedAt = new Date().toISOString();
|
||||
const saved = ns.LocalRoutes.save({
|
||||
...routePackage,
|
||||
route: {
|
||||
...routePackage.route,
|
||||
serverRouteId,
|
||||
uploadedAt
|
||||
}
|
||||
});
|
||||
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
|
||||
state.pois = saved.pois;
|
||||
fillRouteForm(state.currentRoute);
|
||||
await loadRoutes();
|
||||
setStatus('Upload erfolgreich.');
|
||||
} finally {
|
||||
if (!state.currentRoute?.serverRouteId) setDisabled(uploadButton, false);
|
||||
}
|
||||
}
|
||||
|
||||
function currentPosition() {
|
||||
return typeof ns.App?.currentPosition === 'function' ? ns.App.currentPosition() : null;
|
||||
}
|
||||
@@ -1072,6 +1206,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
activate(byId(ids.close), closeEditor);
|
||||
activate(byId(ids.newRoute), newRoute);
|
||||
activate(byId(ids.routeBack), () => guarded(loadRoutes).then(showListView));
|
||||
activate(byId(ids.routeUpload), () => guarded(uploadLocalRoute));
|
||||
activate(byId(ids.addPoi), newPoi);
|
||||
activate(byId(ids.poiAudioAdd), () => openFilePicker(ids.poiAudio));
|
||||
activate(byId(ids.poiImagesAdd), () => openFilePicker(ids.poiImages));
|
||||
|
||||
Reference in New Issue
Block a user