Multiple improvements
Sonarqube Scanner / Prepare environment (push) Successful in 10s
Sonarqube Scanner / Build and test (push) Successful in 28s
Sonarqube Scanner / SonarQube analysis (push) Successful in 1m8s
Sonarqube Scanner / Deploy to Server (push) Successful in 8s

This commit is contained in:
2026-10-05 01:18:14 +02:00
parent 2b07e9bc47
commit 49a9de0355
27 changed files with 797 additions and 80 deletions
+21 -5
View File
@@ -465,12 +465,18 @@
background: #fff;
border: 1px solid rgba(49, 93, 58, .18);
border-radius: .9rem;
display: flex;
display: grid;
gap: .75rem;
justify-content: space-between;
grid-template-columns: minmax(0, 1fr) auto;
padding: .75rem;
}
.editor-route-item > :first-child,
.editor-poi-item > :first-child {
min-width: 0;
overflow-wrap: anywhere;
}
.editor-route-item strong,
.editor-poi-item strong {
display: block;
@@ -517,16 +523,23 @@
}
.editor-icon-row {
display: flex;
flex: 0 0 auto;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 2.75rem;
gap: .35rem;
justify-self: end;
}
.editor-icon-button {
align-items: center;
display: inline-flex;
justify-content: center;
min-width: 2.4rem;
box-sizing: border-box;
width: 2.75rem;
min-width: 2.75rem;
height: 2.75rem;
margin: 0;
padding: .35rem;
}
.editor-icon-danger {
@@ -577,3 +590,6 @@
grid-template-columns: 1fr;
}
}
.current-location-button { min-width: 44px; min-height: 44px; }
.current-location-button img { width: 24px; height: 24px; vertical-align: middle; }
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#244c34" stroke-width="2"><circle cx="12" cy="12" r="7"/><circle cx="12" cy="12" r="2"/><path d="M12 1v5m0 12v5M1 12h5m12 0h5"/></svg>

After

Width:  |  Height:  |  Size: 215 B

+14 -4
View File
@@ -319,6 +319,7 @@
<p><span>Datenschutzangaben der Schule:</span><br>
<strong>Staatliches Schulamt Südthüringen</strong><br><span>Datenschutzbeauftragter</span><br><span>Hölderlinstraße 1</span><br><span>98527 Suhl</span><br><span>Telefon: 03681 / 7341 25</span><br><span>Fax: 03681 / 7341 09</span><br><span>E-Mail: datenschutz.suedthueriungen@schulamt.thueringen.de</span></p>
<p>Der Datenschutzbeauftragte kann außerdem über die Postadresse der Schule mit dem Zusatz „der Datenschutzbeauftragte“ erreicht werden.</p>
<p>Für Fragen zu Wegwichtel, Datenschutzanfragen, Widerruf und Entfernung von Aufnahmen erreichst du die Betreiber unter <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a>.</p>
<h3>2. Wenn du Wegwichtel öffnest</h3>
<p>Wenn du Wegwichtel öffnest, können Verbindungsdaten verarbeitet werden. Dazu gehören zum Beispiel die Internetadresse deines Geräts, Datum, Uhrzeit und Zeitzone, die aufgerufene Seite, übertragene Daten und ob der Aufruf geklappt hat. Auch die vorherige Seite und Angaben zu deinem Gerät, Internetbrowser und dessen Sprache können dazugehören. Diese Daten helfen dabei, die Website bereitzustellen, Fehler zu finden und sie zu schützen.</p>
<p><span>Wer die Website bereitstellt und welche Verbindungsdaten wie lange gespeichert werden:</span><strong>[Anbieter, Anschrift, gespeicherte Daten und Speicherdauer ergänzen]</strong></p>
@@ -348,7 +349,7 @@
<p>Die Nutzungsvereinbarung regelt, welche Nutzungsrechte du für diese Zwecke einräumst. Die Urheberrechte bleiben bei den jeweiligen Urhebern. Eine Nutzungserlaubnis für ein Bild oder eine Tonaufnahme ersetzt keine erforderliche Einwilligung der darin erkennbaren oder hörbaren Personen. Diese Datenschutzerklärung allein erteilt keine solche Erlaubnis.</p>
<p>Personenbezogene Aufnahmen werden nur veröffentlicht, wenn eine passende Rechtsgrundlage vorliegt. Soweit eine Einwilligung erforderlich ist, muss sie vor der Veröffentlichung eingeholt werden. Bei Kindern sind insbesondere die erforderlichen Zustimmungen ihrer Sorgeberechtigten zu beachten. Eine Einwilligung kann jederzeit für die Zukunft widerrufen werden; die Rechtmäßigkeit der bis dahin erfolgten Verarbeitung bleibt davon unberührt.</p>
<p>Bei einem Widerruf oder einer berechtigten Löschanfrage prüfen die Betreiber die weitere Nutzung und entfernen die betroffenen Aufnahmen, soweit dies rechtlich erforderlich ist, aus der öffentlichen Bereitstellung. Bereits gespeicherte Kopien auf fremden Geräten können sie nicht vollständig zurückholen. Nicht öffentliche Sicherungskopien und Nachweise dürfen nur im rechtlich erforderlichen Umfang und für die dafür festgelegte Dauer aufbewahrt werden.</p>
<p><strong>[Vor Veröffentlichung ergänzen: Rechtsgrundlagen für Einsendung, Prüfung, Veröffentlichung und Rechtenachweise; Empfänger und Hostinganbieter; Lösch- und Sicherungsfristen; Kontakt für Widerruf und Entfernung.]</strong></p>
<p><strong>[Vor Veröffentlichung ergänzen: Rechtsgrundlagen für Einsendung, Prüfung, Veröffentlichung und Rechtenachweise; Empfänger und Hostinganbieter; Lösch- und Sicherungsfristen.]</strong></p>
<h3>9. Deine Rechte</h3>
<p>Du kannst fragen, welche Daten über dich gespeichert sind. Unter den gesetzlichen Voraussetzungen kannst du verlangen, dass sie berichtigt, gelöscht, nur noch eingeschränkt genutzt oder dir zur Weitergabe bereitgestellt werden. Du kannst der Nutzung widersprechen und eine erteilte Erlaubnis für die Zukunft zurücknehmen. Wenn du dich über den Umgang mit deinen Daten beschweren möchtest, kannst du dich an die folgende Datenschutzbehörde wenden.</p>
<p><span>Zuständige Datenschutzaufsicht:</span><br>
@@ -361,6 +362,7 @@
<strong>Staatliche Grundschule Herpf</strong><br><span>Zum Eichig 3</span><br><span>98617 Meiningen / OT Herpf</span></p>
<h3>Kontakt</h3>
<p><span>Telefon: (036943) 63722</span><br><span>Telefax: (036943) 63843</span><br><span>E-Mail: info@grundschule-herpf.de</span></p>
<p>Kontakt für Wegwichtel: <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a></p>
<h3>Leitung der Schule</h3>
<p>Frau Wichert, Schulleiterin</p>
<h3>Schulträger</h3>
@@ -369,7 +371,7 @@
<h3>Inhaltlich verantwortlich</h3>
<p>Frau Wichert</p>
<h3>Technisch verantwortlich</h3>
<p>Florian Zumpe<br><span>[Anschrift von Florian Zumpe ergänzen]</span><br><span>E-Mail: [E-Mail-Adresse von Florian Zumpe ergänzen]</span></p>
<p>Florian Zumpe<br><span>[Anschrift von Florian Zumpe ergänzen]</span><br><span>E-Mail: <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a></span></p>
<h3>Weitere Pflichtangaben</h3>
<p>Je nachdem, wer Wegwichtel betreibt, können weitere Angaben nötig sein. Dazu gehören zum Beispiel die Verantwortlichen für den Betrieb der Website. Diese Angaben müssen noch geprüft werden.</p>
<h3>Haftung für Inhalte und Verweise</h3>
@@ -380,7 +382,7 @@
<p>Die Urheberrechte an Bildern, Tonaufnahmen und anderen Beiträgen bleiben bei den jeweiligen Urhebern. Die Betreiber nutzen eingesandte Aufnahmen im Rahmen der vereinbarten Nutzungsrechte. Die Veröffentlichung erlaubt anderen Personen keine freie Weiterverwendung. Gesetzlich erlaubte Nutzungen und ausdrücklich erteilte Lizenzen bleiben unberührt.</p>
</article>
<article class="info-page" data-info-page="terms" lang="de" hidden>
<p class="legal-warning"><strong>Entwurf:</strong> Diese Vereinbarung muss vor ihrer verbindlichen Verwendung rechtlich geprüft werden. Betreiber, Kontakt und das Verfahren zur Zustimmung und Urhebernennung sind noch zu ergänzen.</p>
<p class="legal-warning"><strong>Entwurf:</strong> Diese Vereinbarung muss vor ihrer verbindlichen Verwendung rechtlich geprüft werden. Betreiber und das Verfahren zur Zustimmung und Urhebernennung sind noch zu ergänzen.</p>
<p>Stand: 4. Oktober 2026</p>
<h3>1. Für wen diese Vereinbarung gilt</h3>
<p>Diese Vereinbarung regelt das Einsenden von Bildern und Tonaufnahmen an Wegwichtel. Nutzungsrechte werden dem Betreiber <strong>[vollständiger Name und Anschrift des tatsächlichen Betreibers ergänzen]</strong> eingeräumt. Die bloße Nutzung der App oder das Speichern eines Entwurfs auf deinem Gerät räumt keine Rechte ein.</p>
@@ -395,7 +397,7 @@
<p>Du darfst nur Aufnahmen senden, die du selbst erstellt hast oder für die du alle erforderlichen Erlaubnisse besitzt, um diese Nutzungsrechte einzuräumen. Bei Tonaufnahmen sind auch die Rechte an Musik, gesprochenen Texten, Darbietungen und der Aufnahme selbst zu beachten. Die Erlaubnis zum privaten Anhören oder zum eigenen Gebrauch reicht für eine Veröffentlichung in Wegwichtel nicht aus.</p>
<p>Auch die Rechte erkennbarer oder hörbarer Personen müssen beachtet werden. Eine Nutzungserlaubnis des Fotografen ersetzt nicht die erforderliche Einwilligung der abgebildeten Person. Kinder brauchen für die Rechteeinräumung die erforderliche Zustimmung ihrer gesetzlichen Vertreter. Sorgeberechtigte dürfen Rechte anderer Urheber nicht ohne deren Erlaubnis einräumen.</p>
<h3>5. Prüfung, Entfernung und Beschwerden</h3>
<p>Die Betreiber sind nicht verpflichtet, eingesandte Aufnahmen zu veröffentlichen. Wenn Rechte unklar sind oder eine berechtigte Beanstandung vorliegt, dürfen sie die Veröffentlichung aussetzen oder die Aufnahme entfernen. Fragen zu Urheberschaft, Nutzungsrechten, Namensnennung oder Entfernung richtest du an <strong>[Kontakt für Rechtefragen ergänzen]</strong>. Datenschutzrechtliche Einwilligungen und Widerrufe werden getrennt von den urheberrechtlichen Nutzungsrechten geprüft.</p>
<p>Die Betreiber sind nicht verpflichtet, eingesandte Aufnahmen zu veröffentlichen. Wenn Rechte unklar sind oder eine berechtigte Beanstandung vorliegt, dürfen sie die Veröffentlichung aussetzen oder die Aufnahme entfernen. Fragen zu Urheberschaft, Nutzungsrechten, Namensnennung oder Entfernung richtest du an <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a>. Datenschutzrechtliche Einwilligungen und Widerrufe werden getrennt von den urheberrechtlichen Nutzungsrechten geprüft.</p>
<h3>6. Hinweis bei ausgelieferten Bildern</h3>
<p>Der technische Rechtehinweis bei Bildern informiert über diese Nutzungsregeln. Er ersetzt weder die Urhebernennung bei der Aufnahme noch einen Nachweis über die tatsächlich erteilte Nutzungserlaubnis. Er verhindert auch nicht das Kopieren einer Aufnahme.</p>
</article>
@@ -485,6 +487,14 @@
<input id="editor-route-gpx" type="file" accept=".gpx,application/gpx+xml,text/xml,application/xml">
</div>
<section id="editor-recording-options">
<label class="switch-row" for="route-recording-toggle">
<span data-i18n="recorder.toggle">Meinen Weg aufnehmen</span>
<input id="route-recording-toggle" type="checkbox" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"></span>
</label>
<p class="editor-muted" data-i18n="recorder.toggle-help">Alle zwei Minuten wird dein Standort auf diesem Gerät gespeichert. Die Aufnahme endet, wenn du die Bearbeitung verlässt.</p>
</section>
<details id="editor-path-positions">
<summary data-i18n="recorder.edit-path">Orte auf dem Weg ergänzen</summary>
<p data-i18n="recorder.edit-help">Fehlende Orte kannst du später eintragen. Leere Felder bleiben gespeichert.</p>
+39 -1
View File
@@ -22,6 +22,19 @@
persistTimer: null
};
let poiMediaGeneration = 0;
let localPoiMedia = null;
function releaseLocalPoiMedia() {
poiMediaGeneration += 1;
if (localPoiMedia) {
ns.AudioPlayer.unload();
ns.Slideshow.show([]);
ns.LocalRoutes.releasePoiMedia(localPoiMedia);
localPoiMedia = null;
}
}
function escapeHtml(value) {
return $('<div>').text(value ?? '').html();
}
@@ -88,6 +101,7 @@
const target = $(pageSelector);
if (!target.length) throw new Error(ns.I18n.t("app.page.missing"));
if (pageSelector !== '#poi-page') releaseLocalPoiMedia();
const previous = state.activePage;
$('.app-page').attr('hidden', true);
target.removeAttr('hidden');
@@ -287,6 +301,7 @@
}
function endRoute({ announce = true, returnToRoutes = false } = {}) {
releaseLocalPoiMedia();
const wasActive = state.routeState !== 'idle';
state.routeState = 'idle';
persistRouteProgress();
@@ -330,6 +345,7 @@
}
function suspendCurrentRoute() {
releaseLocalPoiMedia();
if (!state.route) return;
if (state.routeState === 'running') state.routeState = 'paused';
persistRouteProgress();
@@ -383,6 +399,7 @@
}
function showPoi(poi) {
releaseLocalPoiMedia();
state.activePoiId = Number(poi.id);
state.viewed.add(state.activePoiId);
persistRouteProgress();
@@ -392,6 +409,27 @@
ns.Slideshow.show(poi.images);
ns.AudioPlayer.load(poi.audioUrl);
navigate('#poi-page');
if (state.route?.source === 'local') void loadLocalPoiMedia(poi);
}
async function loadLocalPoiMedia(poi) {
const route = state.route;
const requestGeneration = poiMediaGeneration;
try {
const loaded = await ns.LocalRoutes.getAppPoi(route.id, poi.id, { requestPermission: true });
if (requestGeneration !== poiMediaGeneration || state.route !== route) {
ns.LocalRoutes.releasePoiMedia(loaded);
return;
}
if (!loaded) return;
localPoiMedia = loaded;
ns.Slideshow.show(loaded.images);
ns.AudioPlayer.load(loaded.audioUrl);
} catch (error) {
if (requestGeneration === poiMediaGeneration) {
$('#tracking-status').prop('hidden', false).text(ns.UserMessages?.error(error) || ns.I18n.t('app.error.open-draft'));
}
}
}
function nearestRoutePointIndex(position) {
@@ -680,7 +718,7 @@
async function selectRoute(source, id) {
setRouteControlOpen(false);
const route = source === 'local'
? await ns.LocalRoutes.getAppRoute(id, { requestPermission: true })
? await ns.LocalRoutes.getAppRoute(id)
: { ...(await ns.Api.route(id)), source: 'server', local: false };
renderRoute(route);
navigate('#route-page');
+73 -24
View File
@@ -202,7 +202,17 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
return ns.I18n.t("editor.view-route");
}
let poiMediaGeneration = 0;
let previewPoi = null;
function releasePoiPreview() {
poiMediaGeneration += 1;
ns.LocalRoutes?.releasePoiMedia?.(previewPoi);
previewPoi = null;
}
function showListView() {
releasePoiPreview();
ns.RouteRecorder?.endEditing();
setDialogTitle('Streckenverwaltung');
setHidden(byId(ids.routeListView), false);
@@ -211,6 +221,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function showRouteView() {
releasePoiPreview();
setDialogTitle(routeDialogTitle());
setHidden(byId(ids.routeListView), true);
setHidden(byId(ids.routeDetailView), false);
@@ -411,8 +422,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
list.textContent = '';
const serverRoutes = state.routes.filter(route => routeSource(route) === 'server');
const localRoutes = state.routes.filter(route => routeSource(route) === 'local');
renderRouteGroup(list, ns.I18n.t("editor.public-routes"), serverRoutes, ns.I18n.t("app.no-public-route"));
renderRouteGroup(list, ns.I18n.t("editor.drafts"), localRoutes, ns.I18n.t("editor.no-draft"));
renderRouteGroup(list, ns.I18n.t("editor.public-routes"), serverRoutes, ns.I18n.t("app.no-public-route"));
}
function renderPoiList() {
@@ -438,19 +449,30 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}));
}
let routeListGeneration = 0;
async function loadRoutes() {
setStatus(ns.I18n.t("editor.loading-routes"));
const payload = await readJson('/routes?activatedOnly=true');
const serverRoutes = routeArray(payload).map(route => ({ ...route, _source: 'server' }));
let localRoutes = [];
try {
localRoutes = localRouteSummaries();
} catch (error) {
setStatus(error.message, true);
}
state.routes = [...serverRoutes, ...localRoutes];
const requestGeneration = ++routeListGeneration;
state.routes = [...localRouteSummaries(), ...state.routes.filter(route => routeSource(route) === 'server')];
renderRouteList();
if (!byId(ids.status)?.dataset.kind || byId(ids.status).dataset.kind !== 'error') setStatus('');
setStatus('');
// Local drafts are usable even while this request is waiting for a connection.
void refreshPublicRoutes(requestGeneration);
}
async function refreshPublicRoutes(requestGeneration) {
const listIsVisible = () => byId(ids.dialog)?.open && !byId(ids.routeListView)?.hidden;
try {
const payload = await readJson('/routes?activatedOnly=true');
if (requestGeneration !== routeListGeneration) return;
const serverRoutes = routeArray(payload).map(route => ({ ...route, _source: 'server' }));
state.routes = [...localRouteSummaries(), ...serverRoutes];
renderRouteList();
} catch {
if (requestGeneration === routeListGeneration && listIsVisible()) {
setStatus(ns.I18n.t('editor.public-unavailable'));
}
}
}
async function openEditor() {
@@ -471,6 +493,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function closeEditor() {
releasePoiPreview();
ns.RouteRecorder?.endEditing();
const dialog = byId(ids.dialog);
if (dialog?.open && typeof dialog.close === 'function') dialog.close();
@@ -499,6 +522,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function fillRouteForm(route) {
setHidden(byId('editor-path-positions'), state.currentSource !== 'local');
setHidden(byId('editor-recording-options'), state.currentSource !== 'local');
if (state.currentSource === 'local') ns.RouteRecorder?.renderPoints(route.gpx || ns.LocalRoutes.emptyGpx(route.name));
byId(ids.routeId).value = routeId(route);
byId(ids.routeName).value = routeName(route) === ns.I18n.t("editor.untitled-route") ? '' : routeName(route);
@@ -764,14 +788,14 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function loadRoute(route) {
const id = routeId(route);
if (!id) return;
if (ns.RouteRecorder?.routeId && (routeSource(route) !== 'local' || ns.RouteRecorder.routeId !== id)) {
ns.RouteRecorder.endEditing();
if (state.currentRoute && (routeSource(route) !== state.currentSource || routeId(state.currentRoute) !== id)) {
ns.RouteRecorder?.endEditing();
}
setStatus(ns.I18n.t("editor.route.loading"));
state.currentSource = routeSource(route);
if (state.currentSource === 'local') {
const routePackage = await ns.LocalRoutes.getWithMedia(id, { requestPermission: true });
const routePackage = ns.LocalRoutes.get(id);
if (!routePackage) throw new Error(ns.I18n.t("editor.error.draft-missing"));
state.currentRoute = {
...routePackage.route,
@@ -1048,6 +1072,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function newPoi() {
releasePoiPreview();
const position = currentPosition();
const lat = position?.lat ?? null;
const lon = position?.lon ?? null;
@@ -1065,11 +1090,26 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
showPoiView();
}
function editPoi(poi) {
async function editPoi(poi) {
if (!poi) return;
releasePoiPreview();
state.currentPoi = poi;
fillPoiForm(poi);
byId(ids.poiFormTitle).textContent = ns.I18n.t("editor.edit-station");
showPoiView();
if (state.currentSource !== 'local') return;
const requestGeneration = poiMediaGeneration;
const hydrated = await ns.LocalRoutes.getPoiWithMedia(routeId(state.currentRoute), poi.id, { requestPermission: true });
if (requestGeneration !== poiMediaGeneration || state.currentPoi !== poi) {
ns.LocalRoutes.releasePoiMedia(hydrated);
return;
}
if (!hydrated) return;
previewPoi = hydrated;
state.currentPoi = hydrated;
// Preserve any text edits and newly selected files while existing previews load.
renderPoiMediaPreview();
updatePoiMediaStatus();
}
async function mediaFromLocalForm(existingPoi, poiIdentifier) {
@@ -1122,13 +1162,16 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
route: state.currentRoute,
pois: state.pois
});
const hydrated = await ns.LocalRoutes.getWithMedia(id) || saved;
state.currentRoute = { ...hydrated.route, id: hydrated.id, gpx: hydrated.gpx, _source: 'local' };
state.pois = hydrated.pois;
const previousPoi = state.currentPoi;
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 hydrated;
if (state.currentPoi && previousPoi) {
state.currentPoi = { ...state.currentPoi, audio: previousPoi.audio, images: previousPoi.images };
}
return saved;
}
async function saveLocalPoi(values) {
@@ -1241,7 +1284,10 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
updateCurrentPoi(updated);
await persistLocalCurrentRoute();
if (removedMedia) await ns.LocalRoutes.removeMedia(removedMedia);
if (removedMedia) {
ns.LocalRoutes.releasePoiMedia({ audio: removedMedia });
await ns.LocalRoutes.removeMedia(removedMedia);
}
state.selectedMediaKey = null;
renderPoiMediaPreview();
updatePoiMediaStatus();
@@ -1416,7 +1462,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function poiListAction(event) {
const action = event.target.closest('[data-editor-action]')?.dataset.editorAction;
if (action === 'edit-poi') {
editPoi(poiForItem(event.target));
await editPoi(poiForItem(event.target));
return;
}
if (action === 'delete-poi') {
@@ -1442,12 +1488,15 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function bind() {
for (const eventName of ['cancel', 'close']) {
byId(ids.dialog)?.addEventListener(eventName, () => ns.RouteRecorder?.endEditing());
byId(ids.dialog)?.addEventListener(eventName, () => {
releasePoiPreview();
ns.RouteRecorder?.endEditing();
});
}
activate(byId(ids.button), () => guarded(openEditor));
activate(byId(ids.close), closeEditor);
activate(byId(ids.newRoute), newRoute);
activate(byId(ids.routeBack), () => guarded(loadRoutes).then(showListView));
activate(byId(ids.routeBack), () => { showListView(); void guarded(loadRoutes); });
activate(byId(ids.routeUpload), () => guarded(uploadLocalRoute));
activate(byId(ids.rightsOpen), () => ns.LegalUi?.open('terms'));
activate(byId(ids.addPoi), newPoi);
+36 -7
View File
@@ -12,6 +12,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const MEDIA_STORE_NAME = 'media';
let mediaDatabasePromise = null;
let poiCounter = 0;
const objectUrls = new Set();
function storage() {
const target = globalThis.localStorage;
@@ -195,7 +196,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const blob = await mediaBlob(media, options);
if (!blob) return { ...media, unavailable: true };
if (typeof globalThis.URL?.createObjectURL === 'function') {
return { ...media, url: globalThis.URL.createObjectURL(blob), unavailable: false };
const url = globalThis.URL.createObjectURL(blob);
objectUrls.add(url);
return { ...media, url, unavailable: false };
}
const fallback = await fileToDataUrl(blob);
return { ...media, dataUrl: fallback.dataUrl, unavailable: false };
@@ -204,16 +207,39 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
}
async function withMedia(routePackage, options = {}) {
if (!routePackage) return null;
const pois = await Promise.all((routePackage.pois || []).map(async poi => ({
async function poiWithMedia(poi, options = {}) {
if (!poi) return null;
return {
...poi,
audio: await mediaForUse(poi.audio, options),
images: await Promise.all((poi.images || []).map(media => mediaForUse(media, options)))
})));
};
}
function releasePoiMedia(poi) {
const urls = [poi?.audio?.url, poi?.audioUrl, ...(poi?.images || []).map(image => image.url)];
for (const url of urls) {
if (!objectUrls.delete(url)) continue;
globalThis.URL?.revokeObjectURL?.(url);
}
}
async function withMedia(routePackage, options = {}) {
if (!routePackage) return null;
const pois = await Promise.all((routePackage.pois || []).map(poi => poiWithMedia(poi, options)));
return { ...routePackage, pois };
}
async function getPoiWithMedia(id, poiId, options = {}) {
const poi = readPackage(id)?.pois.find(item => String(item.id) === String(poiId));
return poiWithMedia(poi, options);
}
async function getAppPoi(id, poiId, options = {}) {
const poi = await getPoiWithMedia(id, poiId, options);
return poi ? appPoi(poi) : null;
}
function mediaForStorage(media) {
if (!media) return null;
const stored = { ...media };
@@ -709,8 +735,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
return listPackages().map(toAppRoute);
}
async function getAppRoute(id, options = {}) {
const routePackage = await withMedia(readPackage(id), options);
async function getAppRoute(id) {
const routePackage = readPackage(id);
if (!routePackage) throw new Error(ns.I18n.t("local.error.route-missing"));
return toAppRoute(routePackage);
}
@@ -762,6 +788,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
get: readPackage,
getAppRoute,
getWithMedia,
getPoiWithMedia,
getAppPoi,
releasePoiMedia,
gpxForUpload,
list: listPackages,
listAppRoutes,
+66 -9
View File
@@ -1,18 +1,22 @@
globalThis.Wegwichtel ??= {};
(function (ns) {
'use strict';
const INTERVAL_MS = 30000;
const INTERVAL_MS = 120000;
let id = null;
let editingId = null;
let status = 'idle';
let timer = null;
let generation = 0;
let pending = false;
let pendingGeneration = null;
let enabled = false;
let manualGeneration = 0;
let message = '';
let manualFields = [];
let lastPosition = null;
function render() {
const toggle = document.getElementById('route-recording-toggle');
if (toggle) { toggle.checked = enabled; toggle.setAttribute('aria-checked', String(enabled)); }
const control = document.getElementById('route-recording-control');
if (!control) return;
control.hidden = status === 'idle';
@@ -21,9 +25,9 @@ globalThis.Wegwichtel ??= {};
}
async function sample() {
if (pending || status !== 'recording') return;
pending = true;
if (pendingGeneration === generation || status !== 'recording') return;
const currentGeneration = generation;
pendingGeneration = currentGeneration;
try {
let position;
try { position = await ns.Geo.current({ maximumAge: 0 }); }
@@ -40,7 +44,7 @@ globalThis.Wegwichtel ??= {};
ns.StorageUsage?.reportError(error);
message = ns.I18n.t('recorder.save-error');
} finally {
pending = false;
if (pendingGeneration === currentGeneration) pendingGeneration = null;
render();
}
}
@@ -69,6 +73,7 @@ globalThis.Wegwichtel ??= {};
}
function stop() {
enabled = false;
clearTimer();
id = null;
lastPosition = null;
@@ -78,6 +83,7 @@ globalThis.Wegwichtel ??= {};
}
function start(routeId) {
enabled = true;
lastPosition = null;
id = String(routeId);
editingId = id;
@@ -85,16 +91,46 @@ globalThis.Wegwichtel ??= {};
}
function beginEditing(routeId) {
if (editingId === String(routeId)) return;
stop();
start(routeId);
const nextId = String(routeId);
if (editingId === nextId) return;
if (editingId !== null) endEditing();
editingId = nextId;
if (enabled) { id = editingId; resume(); }
render();
}
function setEnabled(value) {
if (!value) { stop(); return; }
enabled = true;
if (editingId) { id = editingId; resume(); }
render();
}
function endEditing() {
editingId = null;
manualGeneration += 1;
stop();
}
async function fillCurrentLocation(field, button, notice) {
if (button.disabled) return;
const currentGeneration = manualGeneration;
button.disabled = true;
notice.textContent = ns.I18n.t('recorder.locating');
try {
const position = await ns.Geo.current({ maximumAge: 0 });
if (currentGeneration !== manualGeneration || !manualFields.includes(field)) return;
if (!Number.isFinite(position.lat) || !Number.isFinite(position.lon) || Math.abs(position.lat) > 90 || Math.abs(position.lon) > 180) throw new Error('Invalid location');
field.lat.value = String(position.lat);
field.lon.value = String(position.lon);
notice.textContent = ns.I18n.t('recorder.location-set');
} catch {
if (currentGeneration === manualGeneration && manualFields.includes(field)) notice.textContent = ns.I18n.t('recorder.location-error');
} finally {
button.disabled = false;
}
}
function addPositionField(position = {}, index = null) {
const container = document.getElementById('editor-recorded-positions');
if (!container) return;
@@ -114,13 +150,30 @@ globalThis.Wegwichtel ??= {};
row.appendChild(label); row.appendChild(input);
fields[key] = input;
}
manualFields.push({ index, original: position, ...fields });
const field = { index, original: position, ...fields };
manualFields.push(field);
const button = document.createElement('button');
button.type = 'button';
button.className = 'current-location-button';
button.setAttribute('aria-label', ns.I18n.t('recorder.use-location'));
button.title = ns.I18n.t('recorder.use-location');
const icon = document.createElement('img');
icon.src = '/images/icons/current-location.svg';
icon.alt = '';
button.appendChild(icon);
const notice = document.createElement('p');
notice.className = 'editor-muted';
notice.setAttribute('aria-live', 'polite');
button.addEventListener('click', () => { void fillCurrentLocation(field, button, notice); });
row.appendChild(button);
row.appendChild(notice);
container.appendChild(row);
}
function renderPoints(gpx) {
const container = document.getElementById('editor-recorded-positions');
if (!container) return;
manualGeneration += 1;
container.replaceChildren();
manualFields = [];
ns.LocalRoutes.recordedPositions(gpx).forEach(addPositionField);
@@ -146,6 +199,10 @@ globalThis.Wegwichtel ??= {};
function bind() {
globalThis.addEventListener?.('pagehide', endEditing);
document.getElementById('route-recording-toggle')?.addEventListener('change', event => setEnabled(event.target.checked));
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && status === 'recording') void sample();
});
const activate = (elementId, action) => {
const element = document.getElementById(elementId);
element?.addEventListener('pointerup', event => {
+8
View File
@@ -129,6 +129,7 @@ globalThis.Wegwichtel ??= {};
"drafts": "Meine Entwürfe",
"no-draft": "Noch kein eigener Entwurf vorhanden.",
"no-stations": "Noch keine Stationen angelegt.",
"public-unavailable": "Die veröffentlichten Strecken sind gerade nicht erreichbar. Deine Entwürfe auf diesem Gerät kannst du weiter bearbeiten.",
"loading-routes": "Strecken werden geladen …",
"submitted": "Erneut zur Prüfung senden",
"rights-hint": "Wenn du Bilder oder Tonaufnahmen sendest, erlaubst du Wegwichtel, sie nach der Nutzungsvereinbarung zu verwenden. Sende nur Aufnahmen, für die du diese Erlaubnis geben darfst. Kinder brauchen die Zustimmung ihrer Sorgeberechtigten.",
@@ -452,6 +453,13 @@ globalThis.Wegwichtel ??= {};
}
},
"recorder": {
"toggle": "Meinen Weg aufnehmen",
"toggle-help": "Alle zwei Minuten wird dein Standort auf diesem Gerät gespeichert. Die Aufnahme endet, wenn du die Bearbeitung verlässt.",
"use-location": "Meinen Standort einsetzen",
"locating": "Dein Standort wird gesucht …",
"location-set": "Dein Standort ist eingetragen. Speichere jetzt deine Änderungen.",
"location-error": "Dein Standort konnte nicht gefunden werden. Erlaube den Zugriff auf deinen Standort und versuche es noch einmal.",
"recording": "Dein Weg wird aufgenommen.",
"paused": "Die Aufnahme macht eine Pause.",
"pause": "Aufnahme pausieren",
+8
View File
@@ -129,6 +129,7 @@ globalThis.Wegwichtel ??= {};
"drafts": "My drafts",
"no-draft": "There are no drafts yet.",
"no-stations": "There are no stops yet.",
"public-unavailable": "Published trails are currently unavailable. You can still edit your drafts on this device.",
"loading-routes": "Loading trails …",
"submitted": "Send changes for review",
"rights-hint": "By sending pictures or recordings, you allow Wegwichtel to use them under its terms of use. Only send recordings you are allowed to share this way. Children need their parent or guardian’s permission.",
@@ -452,6 +453,13 @@ globalThis.Wegwichtel ??= {};
}
},
"recorder": {
"toggle": "Record my path",
"toggle-help": "Your location is saved on this device every two minutes. Recording stops when you leave the editor.",
"use-location": "Use my current location",
"locating": "Finding your location …",
"location-set": "Your location has been entered. Save your changes now.",
"location-error": "Your location could not be found. Allow access to your location and try again.",
"recording": "Your trail is being recorded.",
"paused": "Recording is paused.",
"pause": "Pause recording",