Navi-Sounds: 2x Wuff = links, 1x Wuff = rechts, Klaeffen = falscher Weg (Idee Rene)

- NavSound (routes.js): WebAudio-Wuff-Synthese (Saegezahn-Sweep + Tiefpass,
  kein Asset, laeuft offline); echte Aufnahmen unter /sounds/wuff.mp3 +
  klaeffen.mp3 (z.B. von Yaro) werden automatisch bevorzugt
- Abbiege-Erkennung: Turns einmalig aus dem Track (Peilung ueber 15-m-
  Stuetzpunkte gegen GPS-Zickzack, >=40 Grad), Ansage bei <=45 m davor,
  einmal pro Abbiegepunkt
- Falscher Weg (>50 m, bestehende Warnung): Klaeffen beim Abkommen +
  Erinnerung alle 30 s; zurueck auf Route reset
- Toggle im Navi-Header (Lautsprecher-Icon, by_nav_sound, Default AN),
  iOS-Audio-Unlock beim Navi-Start + Toggle (Probe-Wuff als Bestaetigung)
Bump v1241
This commit is contained in:
rene 2026-06-06 18:57:20 +02:00
parent a1b2644cce
commit ca23b3ec46
6 changed files with 153 additions and 16 deletions

View file

@ -1 +1 @@
1240
1241

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1240"></script>
<script src="/js/boot-early.js?v=1241"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1240">
<link rel="stylesheet" href="/css/layout.css?v=1240">
<link rel="stylesheet" href="/css/components.css?v=1240">
<link rel="stylesheet" href="/css/utilities.css?v=1240">
<link rel="stylesheet" href="/css/lists.css?v=1240">
<link rel="stylesheet" href="/css/design-system.css?v=1241">
<link rel="stylesheet" href="/css/layout.css?v=1241">
<link rel="stylesheet" href="/css/components.css?v=1241">
<link rel="stylesheet" href="/css/utilities.css?v=1241">
<link rel="stylesheet" href="/css/lists.css?v=1241">
</head>
<body>
@ -612,11 +612,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1240"></script>
<script src="/js/ui.js?v=1240"></script>
<script src="/js/app.js?v=1240"></script>
<script src="/js/worlds.js?v=1240"></script>
<script src="/js/offline-indicator.js?v=1240"></script>
<script src="/js/api.js?v=1241"></script>
<script src="/js/ui.js?v=1241"></script>
<script src="/js/app.js?v=1241"></script>
<script src="/js/worlds.js?v=1241"></script>
<script src="/js/offline-indicator.js?v=1241"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -626,7 +626,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1240"></script>
<script src="/js/boot.js?v=1241"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1240'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1241'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;

View file

@ -68,6 +68,81 @@ window.Page_routes = (() => {
let _navCompassHeading = null;
let _navHeadingSmoothed = null;
// ----------------------------------------------------------
// NAVI-SOUNDS (Idee René 2026-06-06): links = 2× Wuff, rechts = 1× Wuff,
// falscher Weg = Kläffen. WebAudio-Synthese (kein Asset, läuft offline) —
// liegen echte Aufnahmen unter /sounds/wuff.mp3 + /sounds/klaeffen.mp3
// (z.B. von Yaro 🐕), werden DIE bevorzugt. iOS: Audio braucht eine User-Geste
// → unlock() beim Navi-Start/Toggle.
// ----------------------------------------------------------
const NavSound = (() => {
let ctx = null;
let files = null; // { wuff, klaeffen } HTMLAudio | leer = Synthese
const enabled = () => { try { return localStorage.getItem('by_nav_sound') !== '0'; } catch (e) { return true; } };
function _ctx() {
if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
if (ctx.state === 'suspended') ctx.resume().catch(() => {});
return ctx;
}
// Ein synthetischer „Wuff": Sägezahn-Sweep durch Tiefpass, kurzer Attack, schneller Decay.
function _wuff(at, pitch = 1) {
const c = _ctx(), t = c.currentTime + at;
const o = c.createOscillator(), g = c.createGain(), f = c.createBiquadFilter();
o.type = 'sawtooth';
o.frequency.setValueAtTime(240 * pitch, t);
o.frequency.exponentialRampToValueAtTime(75 * pitch, t + 0.16);
f.type = 'lowpass'; f.frequency.value = 900 * pitch; f.Q.value = 3;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.9, t + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
o.connect(f); f.connect(g); g.connect(c.destination);
o.start(t); o.stop(t + 0.25);
}
function _barks(n, pitch, gap) {
if (!enabled()) return;
const sample = files && (pitch > 1.3 ? files.klaeffen : files.wuff);
if (sample) { // echte Aufnahme: n-mal hintereinander
let i = 0;
const play = () => {
if (i++ >= n) return;
sample.currentTime = 0;
sample.play().catch(() => {});
setTimeout(play, gap * 1000 + 200);
};
play();
return;
}
try { for (let i = 0; i < n; i++) _wuff(i * gap, pitch); } catch (e) {}
}
return {
enabled,
unlock() { // in User-Geste aufrufen (iOS-Autoplay-Policy)
try {
const c = _ctx();
const b = c.createBuffer(1, 1, 22050), s = c.createBufferSource();
s.buffer = b; s.connect(c.destination); s.start(0);
} catch (e) {}
// Echte Samples einmalig anfragen (404 → Synthese bleibt)
if (files === null) {
files = {};
['wuff', 'klaeffen'].forEach(name => {
const a = new Audio(`/sounds/${name}.mp3`);
a.preload = 'auto';
a.addEventListener('canplaythrough', () => { files[name] = a; }, { once: true });
a.addEventListener('error', () => {}, { once: true });
});
}
},
links() { _barks(2, 1.0, 0.30); }, // 2× Wuff
rechts() { _barks(1, 1.0, 0.30); }, // 1× Wuff
klaeffen() { _barks(4, 1.7, 0.16); }, // schnelles, höheres Bellen
};
})();
let _navSndAnnouncedIdx = -1; // bis zu welchem Track-Index Abbiegungen angesagt wurden
let _navSndOffRoute = false; // Off-Route-Zustand (Kläffen beim Eintritt + alle 30 s)
let _navSndLastKlaeff = 0;
// Recording-Overlay state
let _recOvl = null, _recMap = null;
let _recFollow = true; // Karte folgt dem Standort bei Aufzeichnung (Drag pausiert)
@ -1670,6 +1745,12 @@ window.Page_routes = (() => {
const track = route.gps_track || [];
if (track.length < 2) return;
// Navi-Sounds: Audio in der User-Geste freischalten (iOS) + Ansage-Status zurücksetzen
NavSound.unlock();
_navSndAnnouncedIdx = -1;
_navSndOffRoute = false;
_navSndLastKlaeff = 0;
_navMaxIdx = 0;
_navRecorded = false;
_navLastBearing = null;
@ -1689,6 +1770,7 @@ window.Page_routes = (() => {
background:var(--c-surface);border-bottom:1px solid var(--c-border);flex-shrink:0">
<button id="rk-nav-back" style="background:none;border:none;color:var(--c-primary);font-size:16px;cursor:pointer;padding:4px 0"> Zurück</button>
<span style="font-weight:700;font-size:14px;flex:1;text-align:center;margin:0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(route.name)}</span>
<button id="rk-nav-sound-btn" style="background:none;border:none;color:${NavSound.enabled() ? 'var(--c-primary)' : 'var(--c-text-secondary)'};cursor:pointer;padding:4px 6px;display:flex;align-items:center" title="Navi-Sounds (2× Wuff = links, 1× = rechts, Kläffen = falscher Weg)">${UI.icon(NavSound.enabled() ? 'speaker-high' : 'speaker-none')}</button>
<button id="rk-nav-center-btn" style="background:none;border:none;color:var(--c-primary);cursor:pointer;padding:4px 6px;display:flex;align-items:center" title="Auf Standort zentrieren">${UI.icon('crosshair')}</button>
</div>
@ -1894,6 +1976,27 @@ window.Page_routes = (() => {
return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
};
// Abbiegepunkte EINMALIG aus dem Track ableiten: Peilung über ~15-m-Stützpunkte
// (sonst macht GPS-Zickzack aus jeder Geraden eine Kurve), Richtungsänderung ≥ 40°
// = Abbiegung, > 0 = rechts. Mindestabstand zwischen Ansagen ~25 m.
const _navTurns = (() => {
const out = [];
const distM = (a, b) => _haversineKm(a.lat, a.lon, b.lat, b.lon) * 1000;
let lastIdx = -1;
for (let i = 1; i < track.length - 1; i++) {
let p = i - 1, accP = distM(track[p], track[i]);
while (p > 0 && accP < 15) { p--; accP += distM(track[p], track[p + 1]); }
let n = i + 1, accN = distM(track[i], track[n]);
while (n < track.length - 1 && accN < 15) { n++; accN += distM(track[n - 1], track[n]); }
const d = ((_bearingTo(track[i], track[n]) - _bearingTo(track[p], track[i]) + 540) % 360) - 180;
if (Math.abs(d) >= 40 && (lastIdx < 0 || distM(track[lastIdx], track[i]) > 25)) {
out.push({ idx: i, right: d > 0 });
lastIdx = i;
}
}
return out;
})();
const _updateStats = (idx, distToRoute, userLat, userLon) => {
if (idx > _navMaxIdx) {
_navMaxIdx = idx;
@ -1924,12 +2027,33 @@ window.Page_routes = (() => {
_navLastBearing = _bearingTo({ lat: userLat, lon: userLon }, track[idx + 1]);
_updateDimArrow();
}
// Abbiege-Ansage (René: 2× Wuff = links, 1× = rechts): nächster Turn vor uns,
// angesagt sobald ≤ 45 m entfernt — einmal pro Abbiegepunkt.
if (userLat != null && distToRoute < 0.1) {
const next = _navTurns.find(t => t.idx > idx && t.idx > _navSndAnnouncedIdx);
if (next) {
const dM = _haversineKm(userLat, userLon, track[next.idx].lat, track[next.idx].lon) * 1000;
if (dM <= 45) {
_navSndAnnouncedIdx = next.idx;
if (next.right) NavSound.rechts(); else NavSound.links();
}
}
}
const offWarn = document.getElementById('rk-nav-offwarn');
if (distToRoute * 1000 > 50) {
offWarn.style.display = '';
if (navigator.vibrate) navigator.vibrate([200, 100, 200]);
// Falscher Weg = Kläffen (beim Abkommen + Erinnerung alle 30 s)
const _now = Date.now();
if (!_navSndOffRoute || _now - _navSndLastKlaeff > 30000) {
_navSndOffRoute = true;
_navSndLastKlaeff = _now;
NavSound.klaeffen();
}
} else {
offWarn.style.display = 'none';
_navSndOffRoute = false;
}
// Polylines aktualisieren
doneLine.setLatLngs(track.slice(0, idx + 1).map(p => [p.lat, p.lon]));
@ -2057,6 +2181,19 @@ window.Page_routes = (() => {
if (locMarker) _navMap.setView(locMarker.getLatLng(), 16);
});
// Navi-Sounds an/aus (Klick = User-Geste → unlock + Probe-Wuff als Bestätigung)
document.getElementById('rk-nav-sound-btn')?.addEventListener('click', e => {
const on = !NavSound.enabled();
try { localStorage.setItem('by_nav_sound', on ? '1' : '0'); } catch (err) {}
const btn = e.currentTarget;
btn.style.color = on ? 'var(--c-primary)' : 'var(--c-text-secondary)';
btn.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${on ? 'speaker-high' : 'speaker-none'}`);
if (on) { NavSound.unlock(); NavSound.rechts(); }
UI.toast.info(on
? 'Navi-Sounds an: 2× Wuff = links, 1× Wuff = rechts, Kläffen = falscher Weg 🐕'
: 'Navi-Sounds aus.');
});
document.getElementById('rk-nav-pois')?.addEventListener('click', () => {
if (!_navPois.length) { UI.toast.info('Keine POIs entlang dieser Route.'); return; }
const byType = {};

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1240"></script>
<script src="/js/landing-init.js?v=1241"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1240';
const VER = '1241';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten