This commit is contained in:
+120
-12
@@ -152,6 +152,7 @@
|
||||
.editor-button,
|
||||
.editor-primary-button,
|
||||
.editor-secondary-button,
|
||||
.editor-danger-button,
|
||||
.editor-icon-button {
|
||||
border-radius: .75rem;
|
||||
font: inherit;
|
||||
@@ -216,6 +217,122 @@
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.editor-media-preview {
|
||||
display: grid;
|
||||
gap: .65rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(6.25rem, 1fr));
|
||||
}
|
||||
|
||||
.editor-media-preview:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editor-media-preview-card {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(49, 93, 58, .22);
|
||||
border-radius: .8rem;
|
||||
min-width: 0;
|
||||
padding: .35rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-media-preview-card[data-selected="true"] {
|
||||
border-color: #315d3a;
|
||||
box-shadow: 0 0 0 .12rem rgba(49, 93, 58, .18);
|
||||
}
|
||||
|
||||
.editor-media-preview-select {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #233326;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font: inherit;
|
||||
gap: .35rem;
|
||||
min-height: 5.2rem;
|
||||
padding: .25rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-media-preview-select small {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-media-preview-image {
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: .55rem;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-media-preview-audio {
|
||||
align-items: center;
|
||||
background: #eef5ef;
|
||||
border-radius: .55rem;
|
||||
display: flex;
|
||||
font-size: 2rem;
|
||||
justify-content: center;
|
||||
min-height: 4.1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-media-remove {
|
||||
align-items: center;
|
||||
background: #b42318;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 800;
|
||||
height: 2.25rem;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
position: absolute;
|
||||
right: .15rem;
|
||||
top: .15rem;
|
||||
width: 2.25rem;
|
||||
}
|
||||
|
||||
.editor-media-remove[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editor-form-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .65rem;
|
||||
}
|
||||
|
||||
.editor-form-actions > button {
|
||||
flex: 1 1 11rem;
|
||||
}
|
||||
|
||||
.editor-danger-button {
|
||||
align-items: center;
|
||||
background: #b42318;
|
||||
border: 1px solid #b42318;
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
font-weight: 700;
|
||||
gap: .55rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.editor-danger-icon {
|
||||
color: #fff;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.editor-round-button {
|
||||
align-items: center;
|
||||
background: #315d3a;
|
||||
@@ -236,6 +353,9 @@
|
||||
.editor-round-button:focus-visible,
|
||||
.editor-primary-button:focus-visible,
|
||||
.editor-secondary-button:focus-visible,
|
||||
.editor-danger-button:focus-visible,
|
||||
.editor-media-preview-select:focus-visible,
|
||||
.editor-media-remove:focus-visible,
|
||||
.editor-icon-button:focus-visible {
|
||||
outline: .2rem solid #f2ca6b;
|
||||
outline-offset: .15rem;
|
||||
@@ -383,12 +503,6 @@
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.editor-coordinate-grid {
|
||||
display: grid;
|
||||
gap: .55rem;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.editor-secondary-button,
|
||||
.editor-icon-button {
|
||||
background: #fff;
|
||||
@@ -446,12 +560,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 42rem) {
|
||||
.editor-coordinate-grid {
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 34rem) {
|
||||
.editor-media-actions {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
+11
-10
@@ -391,15 +391,9 @@
|
||||
<label for="editor-poi-description">Text</label>
|
||||
<textarea id="editor-poi-description" rows="6"></textarea>
|
||||
|
||||
<div class="editor-coordinate-grid">
|
||||
<label for="editor-poi-lat">Breite</label>
|
||||
<input id="editor-poi-lat" type="number" inputmode="decimal" step="0.000001" required>
|
||||
<label for="editor-poi-lon">Länge</label>
|
||||
<input id="editor-poi-lon" type="number" inputmode="decimal" step="0.000001" required>
|
||||
|
||||
<label for="editor-poi-radius">Auslöseradius in Metern</label>
|
||||
<input id="editor-poi-radius" type="number" inputmode="numeric" min="1" step="1" value="25">
|
||||
</div>
|
||||
<input id="editor-poi-lat" type="hidden">
|
||||
<input id="editor-poi-lon" type="hidden">
|
||||
<input id="editor-poi-radius" type="hidden" value="25">
|
||||
|
||||
<div class="editor-media-actions" aria-label="Medien zur Station hinzufügen">
|
||||
<button id="editor-poi-audio-add" class="editor-media-button" type="button" title="Audio hinzufügen">
|
||||
@@ -420,9 +414,16 @@
|
||||
</div>
|
||||
<input id="editor-poi-audio" type="file" accept="audio/*" hidden>
|
||||
<input id="editor-poi-images" type="file" accept="image/*" multiple hidden>
|
||||
<div id="editor-poi-media-preview" class="editor-media-preview" aria-label="Medienvorschau"></div>
|
||||
<p id="editor-poi-media-status" class="editor-muted" aria-live="polite"></p>
|
||||
|
||||
<button id="editor-poi-save" class="editor-primary-button" type="submit">Station speichern</button>
|
||||
<div class="editor-form-actions">
|
||||
<button id="editor-poi-save" class="editor-primary-button" type="submit">Station speichern</button>
|
||||
<button id="editor-poi-delete" class="editor-danger-button" type="button" hidden>
|
||||
<span class="editor-danger-icon" aria-hidden="true">−</span>
|
||||
<span>POI entfernen</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+312
-32
@@ -13,6 +13,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
poiAudio: 'editor-poi-audio',
|
||||
poiAudioAdd: 'editor-poi-audio-add',
|
||||
poiCancel: 'editor-poi-cancel',
|
||||
poiDelete: 'editor-poi-delete',
|
||||
poiDescription: 'editor-poi-description',
|
||||
poiForm: 'editor-poi-form',
|
||||
poiFormTitle: 'editor-poi-form-title',
|
||||
@@ -23,6 +24,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
poiLat: 'editor-poi-lat',
|
||||
poiList: 'editor-poi-list',
|
||||
poiLon: 'editor-poi-lon',
|
||||
poiMediaPreview: 'editor-poi-media-preview',
|
||||
poiMediaStatus: 'editor-poi-media-status',
|
||||
poiRadius: 'editor-poi-radius',
|
||||
poiSave: 'editor-poi-save',
|
||||
@@ -45,16 +47,24 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
|
||||
const classes = Object.freeze({
|
||||
button: 'editor-button',
|
||||
coordinateGrid: 'editor-coordinate-grid',
|
||||
dialog: 'editor-dialog',
|
||||
dialogHeader: 'editor-dialog-header',
|
||||
dangerButton: 'editor-danger-button',
|
||||
dangerIcon: 'editor-danger-icon',
|
||||
dialogShell: 'editor-dialog-shell',
|
||||
formActions: 'editor-form-actions',
|
||||
form: 'editor-form',
|
||||
iconButton: 'editor-icon-button',
|
||||
iconRow: 'editor-icon-row',
|
||||
mediaActions: 'editor-media-actions',
|
||||
mediaButton: 'editor-media-button',
|
||||
mediaIcon: 'editor-media-icon',
|
||||
mediaPreview: 'editor-media-preview',
|
||||
mediaPreviewAudio: 'editor-media-preview-audio',
|
||||
mediaPreviewCard: 'editor-media-preview-card',
|
||||
mediaPreviewImage: 'editor-media-preview-image',
|
||||
mediaPreviewSelect: 'editor-media-preview-select',
|
||||
mediaRemove: 'editor-media-remove',
|
||||
muted: 'editor-muted',
|
||||
poiItem: 'editor-poi-item',
|
||||
poiList: 'editor-poi-list',
|
||||
@@ -75,8 +85,11 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
currentPoi: null,
|
||||
currentRoute: null,
|
||||
currentSource: null,
|
||||
pendingAudioFile: null,
|
||||
pendingImages: [],
|
||||
pois: [],
|
||||
routes: []
|
||||
routes: [],
|
||||
selectedMediaKey: null
|
||||
};
|
||||
|
||||
function ready(callback) {
|
||||
@@ -261,6 +274,15 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
async function sendDelete(path) {
|
||||
const response = await fetch(apiUrl(path), {
|
||||
credentials: 'same-origin',
|
||||
headers: { Accept: 'application/json' },
|
||||
method: 'DELETE'
|
||||
});
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
function routeArray(payload) {
|
||||
if (Array.isArray(payload)) return payload;
|
||||
if (Array.isArray(payload?.routes)) return payload.routes;
|
||||
@@ -455,22 +477,131 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
}
|
||||
}
|
||||
|
||||
function resetPendingMedia() {
|
||||
state.pendingAudioFile = null;
|
||||
state.pendingImages = [];
|
||||
state.selectedMediaKey = null;
|
||||
if (byId(ids.poiAudio)) byId(ids.poiAudio).value = '';
|
||||
if (byId(ids.poiImages)) byId(ids.poiImages).value = '';
|
||||
}
|
||||
|
||||
function existingAudioUrl(poi) {
|
||||
return poi?.audio?.dataUrl || poi?.audioUrl || poi?.audio_path || null;
|
||||
}
|
||||
|
||||
function imagePreviewUrl(image) {
|
||||
return image?.dataUrl || image?.url || null;
|
||||
}
|
||||
|
||||
function mediaPreviewCard({ key, kind, label, previewUrl = null, source, mediaId = '', mediaIndex = '' }) {
|
||||
const card = document.createElement('div');
|
||||
card.className = classes.mediaPreviewCard;
|
||||
card.dataset.selected = String(state.selectedMediaKey === key);
|
||||
|
||||
const select = document.createElement('button');
|
||||
select.className = classes.mediaPreviewSelect;
|
||||
select.dataset.editorAction = 'select-media';
|
||||
select.dataset.mediaKey = key;
|
||||
select.type = 'button';
|
||||
select.setAttribute('aria-expanded', String(state.selectedMediaKey === key));
|
||||
select.setAttribute('aria-label', `${label}: Entfernen-Schaltfläche ${state.selectedMediaKey === key ? 'ausblenden' : 'anzeigen'}`);
|
||||
|
||||
if (kind === 'image' && previewUrl) {
|
||||
const image = document.createElement('img');
|
||||
image.className = classes.mediaPreviewImage;
|
||||
image.src = previewUrl;
|
||||
image.alt = '';
|
||||
select.appendChild(image);
|
||||
} else {
|
||||
const audio = document.createElement('span');
|
||||
audio.className = classes.mediaPreviewAudio;
|
||||
audio.setAttribute('aria-hidden', 'true');
|
||||
audio.textContent = '♪';
|
||||
select.appendChild(audio);
|
||||
}
|
||||
appendText(select, 'small', label);
|
||||
|
||||
const remove = document.createElement('button');
|
||||
remove.className = classes.mediaRemove;
|
||||
remove.dataset.editorAction = 'remove-media';
|
||||
remove.dataset.mediaId = text(mediaId);
|
||||
remove.dataset.mediaIndex = text(mediaIndex);
|
||||
remove.dataset.mediaKey = key;
|
||||
remove.dataset.mediaKind = kind;
|
||||
remove.dataset.mediaSource = source;
|
||||
remove.hidden = state.selectedMediaKey !== key;
|
||||
remove.type = 'button';
|
||||
remove.textContent = '−';
|
||||
remove.title = `${label} entfernen`;
|
||||
remove.setAttribute('aria-label', `${label} entfernen`);
|
||||
|
||||
card.append(select, remove);
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderPoiMediaPreview(poi = state.currentPoi) {
|
||||
const preview = byId(ids.poiMediaPreview);
|
||||
if (!preview) return;
|
||||
preview.textContent = '';
|
||||
|
||||
if (existingAudioUrl(poi)) {
|
||||
preview.appendChild(mediaPreviewCard({
|
||||
key: 'existing-audio',
|
||||
kind: 'audio',
|
||||
label: poi?.audio?.name || 'Audioaufnahme',
|
||||
source: 'existing'
|
||||
}));
|
||||
}
|
||||
|
||||
if (state.pendingAudioFile) {
|
||||
preview.appendChild(mediaPreviewCard({
|
||||
key: 'pending-audio',
|
||||
kind: 'audio',
|
||||
label: state.pendingAudioFile.name || 'Neue Audioaufnahme',
|
||||
source: 'pending'
|
||||
}));
|
||||
}
|
||||
|
||||
const images = Array.isArray(poi?.images) ? poi.images : [];
|
||||
images.forEach((image, index) => {
|
||||
preview.appendChild(mediaPreviewCard({
|
||||
key: `existing-image-${image.id ?? index}`,
|
||||
kind: 'image',
|
||||
label: image.name || image.caption || `Bild ${index + 1}`,
|
||||
mediaId: image.id ?? '',
|
||||
mediaIndex: index,
|
||||
previewUrl: imagePreviewUrl(image),
|
||||
source: 'existing'
|
||||
}));
|
||||
});
|
||||
|
||||
state.pendingImages.forEach((pending, index) => {
|
||||
preview.appendChild(mediaPreviewCard({
|
||||
key: pending.key,
|
||||
kind: 'image',
|
||||
label: pending.file?.name || `Neues Bild ${index + 1}`,
|
||||
mediaIndex: index,
|
||||
previewUrl: pending.preview?.dataUrl || null,
|
||||
source: 'pending'
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function updatePoiMediaStatus(poi = state.currentPoi) {
|
||||
const status = byId(ids.poiMediaStatus);
|
||||
if (!status) return;
|
||||
const selectedAudio = byId(ids.poiAudio)?.files?.[0];
|
||||
const selectedImages = Array.from(byId(ids.poiImages)?.files || []);
|
||||
const hasExistingAudio = Boolean(poi?.audio || poi?.audioUrl || poi?.audio_path);
|
||||
const existingAudio = existingAudioUrl(poi) ? 1 : 0;
|
||||
const existingImages = Array.isArray(poi?.images) ? poi.images.length : 0;
|
||||
const audioCount = existingAudio + (state.pendingAudioFile ? 1 : 0);
|
||||
const imageCount = existingImages + state.pendingImages.length;
|
||||
const parts = [];
|
||||
|
||||
if (selectedAudio) parts.push(`Audio ausgewählt: ${selectedAudio.name || 'Audiodatei'}`);
|
||||
else if (hasExistingAudio) parts.push('Audio vorhanden');
|
||||
if (audioCount) parts.push(`${audioCount} Audio${audioCount === 1 ? 'aufnahme' : 'aufnahmen'}`);
|
||||
if (imageCount) parts.push(`${imageCount} Bild${imageCount === 1 ? '' : 'er'}`);
|
||||
|
||||
if (selectedImages.length) parts.push(`${selectedImages.length} Bild${selectedImages.length === 1 ? '' : 'er'} ausgewählt`);
|
||||
else if (existingImages) parts.push(`${existingImages} Bild${existingImages === 1 ? '' : 'er'} vorhanden`);
|
||||
|
||||
status.textContent = parts.length ? parts.join(' · ') : 'Noch keine neuen Medien ausgewählt.';
|
||||
status.textContent = parts.length
|
||||
? `${parts.join(' · ')}. Vorschau antippen, um die Entfernen-Schaltfläche einzublenden.`
|
||||
: 'Noch keine Medien vorhanden.';
|
||||
}
|
||||
|
||||
function openFilePicker(inputId) {
|
||||
@@ -479,17 +610,41 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
}
|
||||
|
||||
function fillPoiForm(poi) {
|
||||
resetPendingMedia();
|
||||
byId(ids.poiId).value = poiId(poi);
|
||||
byId(ids.poiTitle).value = poiTitle(poi) === 'Unbenannte Station' ? '' : poiTitle(poi);
|
||||
byId(ids.poiDescription).value = text(poi?.description || poi?.text);
|
||||
byId(ids.poiLat).value = text(poi?.lat ?? poi?.latitude);
|
||||
byId(ids.poiLon).value = text(poi?.lon ?? poi?.longitude);
|
||||
byId(ids.poiRadius).value = text(poi?.triggerRadiusM ?? poi?.radiusM ?? poi?.radius ?? 25);
|
||||
byId(ids.poiAudio).value = '';
|
||||
byId(ids.poiImages).value = '';
|
||||
setHidden(byId(ids.poiDelete), !poiId(poi));
|
||||
renderPoiMediaPreview(poi);
|
||||
updatePoiMediaStatus(poi);
|
||||
}
|
||||
|
||||
function handleAudioSelection() {
|
||||
state.pendingAudioFile = byId(ids.poiAudio)?.files?.[0] || null;
|
||||
state.selectedMediaKey = null;
|
||||
if (byId(ids.poiAudio)) byId(ids.poiAudio).value = '';
|
||||
renderPoiMediaPreview();
|
||||
updatePoiMediaStatus();
|
||||
}
|
||||
|
||||
async function handleImageSelection() {
|
||||
const files = Array.from(byId(ids.poiImages)?.files || []);
|
||||
for (const file of files) {
|
||||
state.pendingImages.push({
|
||||
file,
|
||||
key: `pending-image-${ns.LocalRoutes.createPoiId()}`,
|
||||
preview: await ns.LocalRoutes.fileToDataUrl(file)
|
||||
});
|
||||
}
|
||||
state.selectedMediaKey = null;
|
||||
if (byId(ids.poiImages)) byId(ids.poiImages).value = '';
|
||||
renderPoiMediaPreview();
|
||||
updatePoiMediaStatus();
|
||||
}
|
||||
|
||||
async function loadRoute(route) {
|
||||
const id = routeId(route);
|
||||
if (!id) return;
|
||||
@@ -602,12 +757,20 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
}
|
||||
|
||||
function newPoi() {
|
||||
state.currentPoi = null;
|
||||
const position = currentPosition();
|
||||
const lat = Number(position?.lat);
|
||||
const lon = Number(position?.lon);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
||||
setStatus('Für einen neuen POI muss zuerst eine aktuelle GPS-Position verfügbar sein.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const accuracy = Number(position?.accuracy);
|
||||
state.currentPoi = null;
|
||||
fillPoiForm({
|
||||
lat: position?.lat ?? '',
|
||||
lon: position?.lon ?? '',
|
||||
triggerRadiusM: 25
|
||||
lat,
|
||||
lon,
|
||||
triggerRadiusM: Number.isFinite(accuracy) && accuracy > 0 ? accuracy : 25
|
||||
});
|
||||
byId(ids.poiFormTitle).textContent = state.currentSource === 'local' ? 'GPX-POI hinzufügen' : 'Station hinzufügen';
|
||||
showPoiView();
|
||||
@@ -621,15 +784,15 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
}
|
||||
|
||||
async function mediaFromLocalForm(existingPoi, poiIdentifier) {
|
||||
const audioFile = byId(ids.poiAudio)?.files?.[0];
|
||||
const imageFiles = Array.from(byId(ids.poiImages)?.files || []);
|
||||
const audio = audioFile ? await ns.LocalRoutes.fileToDataUrl(audioFile) : existingPoi?.audio || null;
|
||||
const audio = state.pendingAudioFile
|
||||
? await ns.LocalRoutes.fileToDataUrl(state.pendingAudioFile)
|
||||
: existingPoi?.audio || null;
|
||||
const images = [...(existingPoi?.images || [])];
|
||||
for (const imageFile of imageFiles) {
|
||||
const image = await ns.LocalRoutes.fileToDataUrl(imageFile);
|
||||
for (const pending of state.pendingImages) {
|
||||
const image = pending.preview || await ns.LocalRoutes.fileToDataUrl(pending.file);
|
||||
images.push({
|
||||
...image,
|
||||
id: `${poiIdentifier}-image-${Date.now()}-${images.length}`,
|
||||
id: `${poiIdentifier}-image-${ns.LocalRoutes.createPoiId()}`,
|
||||
caption: ''
|
||||
});
|
||||
}
|
||||
@@ -639,6 +802,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
async function persistLocalCurrentRoute() {
|
||||
const id = routeId(state.currentRoute);
|
||||
if (!id) throw new Error('Speichere zuerst die Strecke.');
|
||||
const currentPoiId = poiId(state.currentPoi);
|
||||
const saved = ns.LocalRoutes.save({
|
||||
id,
|
||||
gpx: state.currentRoute.gpx,
|
||||
@@ -647,6 +811,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
});
|
||||
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
|
||||
state.pois = saved.pois;
|
||||
state.currentPoi = currentPoiId
|
||||
? state.pois.find(poi => poiId(poi) === currentPoiId) || null
|
||||
: null;
|
||||
return saved;
|
||||
}
|
||||
|
||||
@@ -673,19 +840,20 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
await loadRoute({ id: routeId(state.currentRoute), _source: 'local' });
|
||||
}
|
||||
|
||||
async function uploadOne(path, fieldName, file) {
|
||||
async function uploadOne(path, fieldName, file, method = 'POST') {
|
||||
const data = new FormData();
|
||||
data.append(fieldName, file);
|
||||
return sendForm(path, 'POST', data);
|
||||
return sendForm(path, method, data);
|
||||
}
|
||||
|
||||
async function uploadPoiMedia(currentRouteId, currentPoiId) {
|
||||
const audio = byId(ids.poiAudio)?.files?.[0];
|
||||
const images = Array.from(byId(ids.poiImages)?.files || []);
|
||||
const basePath = `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`;
|
||||
if (audio) await uploadOne(`${basePath}/audio`, 'audio', audio);
|
||||
for (const image of images) {
|
||||
await uploadOne(`${basePath}/pictures`, 'picture', image);
|
||||
if (state.pendingAudioFile) {
|
||||
const method = existingAudioUrl(state.currentPoi) ? 'PUT' : 'POST';
|
||||
await uploadOne(`${basePath}/audio`, 'audio', state.pendingAudioFile, method);
|
||||
}
|
||||
for (const pending of state.pendingImages) {
|
||||
await uploadOne(`${basePath}/pictures`, 'picture', pending.file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +880,116 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
else await saveServerPoi(values);
|
||||
}
|
||||
|
||||
function updateCurrentPoi(updatedPoi) {
|
||||
const identifier = poiId(updatedPoi);
|
||||
const index = state.pois.findIndex(poi => poiId(poi) === identifier);
|
||||
if (index >= 0) state.pois[index] = updatedPoi;
|
||||
state.currentPoi = updatedPoi;
|
||||
}
|
||||
|
||||
async function removePendingMedia(kind, mediaIndex) {
|
||||
if (kind === 'audio') {
|
||||
state.pendingAudioFile = null;
|
||||
} else {
|
||||
const index = Number.parseInt(mediaIndex, 10);
|
||||
if (Number.isInteger(index)) state.pendingImages.splice(index, 1);
|
||||
}
|
||||
state.selectedMediaKey = null;
|
||||
renderPoiMediaPreview();
|
||||
updatePoiMediaStatus();
|
||||
}
|
||||
|
||||
async function removeExistingLocalMedia(kind, mediaIndex) {
|
||||
if (!state.currentPoi) return;
|
||||
const updated = { ...state.currentPoi };
|
||||
if (kind === 'audio') {
|
||||
updated.audio = null;
|
||||
} else {
|
||||
const index = Number.parseInt(mediaIndex, 10);
|
||||
updated.images = [...(state.currentPoi.images || [])];
|
||||
if (Number.isInteger(index)) updated.images.splice(index, 1);
|
||||
}
|
||||
updateCurrentPoi(updated);
|
||||
await persistLocalCurrentRoute();
|
||||
state.selectedMediaKey = null;
|
||||
renderPoiMediaPreview();
|
||||
updatePoiMediaStatus();
|
||||
setStatus(kind === 'audio' ? 'Audioaufnahme entfernt.' : 'Bild entfernt.');
|
||||
}
|
||||
|
||||
async function removeExistingServerMedia(kind, mediaId) {
|
||||
const currentRouteId = routeId(state.currentRoute);
|
||||
const currentPoiId = poiId(state.currentPoi);
|
||||
if (!currentRouteId || !currentPoiId) throw new Error('Der POI ist noch nicht gespeichert.');
|
||||
const basePath = `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`;
|
||||
const updated = { ...state.currentPoi };
|
||||
|
||||
if (kind === 'audio') {
|
||||
await sendDelete(`${basePath}/audio`);
|
||||
updated.audio = null;
|
||||
updated.audioUrl = null;
|
||||
updated.audio_path = null;
|
||||
} else {
|
||||
if (!mediaId) throw new Error('Das Bild besitzt keine Server-ID und kann nicht entfernt werden.');
|
||||
await sendDelete(`${basePath}/pictures/${encodeURIComponent(mediaId)}`);
|
||||
updated.images = (state.currentPoi.images || []).filter(image => text(image.id) !== text(mediaId));
|
||||
}
|
||||
|
||||
updateCurrentPoi(updated);
|
||||
state.selectedMediaKey = null;
|
||||
renderPoiMediaPreview();
|
||||
updatePoiMediaStatus();
|
||||
setStatus(kind === 'audio' ? 'Audioaufnahme entfernt.' : 'Bild entfernt.');
|
||||
}
|
||||
|
||||
async function removeMedia(control) {
|
||||
const kind = control.dataset.mediaKind;
|
||||
const source = control.dataset.mediaSource;
|
||||
if (source === 'pending') {
|
||||
await removePendingMedia(kind, control.dataset.mediaIndex);
|
||||
return;
|
||||
}
|
||||
if (state.currentSource === 'local') {
|
||||
await removeExistingLocalMedia(kind, control.dataset.mediaIndex);
|
||||
return;
|
||||
}
|
||||
await removeExistingServerMedia(kind, control.dataset.mediaId);
|
||||
}
|
||||
|
||||
async function mediaPreviewAction(event) {
|
||||
const control = event.target.closest('[data-editor-action]');
|
||||
const action = control?.dataset.editorAction;
|
||||
if (action === 'select-media') {
|
||||
const key = control.dataset.mediaKey;
|
||||
state.selectedMediaKey = state.selectedMediaKey === key ? null : key;
|
||||
renderPoiMediaPreview();
|
||||
return;
|
||||
}
|
||||
if (action === 'remove-media') await removeMedia(control);
|
||||
}
|
||||
|
||||
async function deleteCurrentPoi() {
|
||||
const currentRouteId = routeId(state.currentRoute);
|
||||
const currentPoiId = poiId(state.currentPoi);
|
||||
if (!currentRouteId || !currentPoiId) return;
|
||||
|
||||
if (state.currentSource === 'local') {
|
||||
state.pois = state.pois
|
||||
.filter(poi => poiId(poi) !== currentPoiId)
|
||||
.map((poi, sequence) => ({ ...poi, sequence }));
|
||||
state.currentPoi = null;
|
||||
await persistLocalCurrentRoute();
|
||||
renderPoiList();
|
||||
showRouteView();
|
||||
setStatus('POI entfernt und lokale Strecke aktualisiert.');
|
||||
return;
|
||||
}
|
||||
|
||||
await sendDelete(`/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`);
|
||||
await loadRoute({ id: currentRouteId, _source: 'server' });
|
||||
setStatus('POI entfernt.');
|
||||
}
|
||||
|
||||
function reorderPois(index, offset) {
|
||||
const target = index + offset;
|
||||
if (target < 0 || target >= state.pois.length) return false;
|
||||
@@ -797,8 +1075,10 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
activate(byId(ids.addPoi), newPoi);
|
||||
activate(byId(ids.poiAudioAdd), () => openFilePicker(ids.poiAudio));
|
||||
activate(byId(ids.poiImagesAdd), () => openFilePicker(ids.poiImages));
|
||||
byId(ids.poiAudio)?.addEventListener('change', () => updatePoiMediaStatus());
|
||||
byId(ids.poiImages)?.addEventListener('change', () => updatePoiMediaStatus());
|
||||
byId(ids.poiAudio)?.addEventListener('change', handleAudioSelection);
|
||||
byId(ids.poiImages)?.addEventListener('change', () => guarded(handleImageSelection));
|
||||
activate(byId(ids.poiMediaPreview), event => guarded(() => mediaPreviewAction(event)));
|
||||
activate(byId(ids.poiDelete), () => guarded(deleteCurrentPoi));
|
||||
activate(byId(ids.poiCancel), showRouteView);
|
||||
activate(byId(ids.routeList), event => guarded(() => routeListAction(event)));
|
||||
activate(byId(ids.poiList), poiListAction);
|
||||
|
||||
Reference in New Issue
Block a user